From b2eb56076d47af08a11e10fa53446a00b849a13c Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 26 Sep 2019 19:45:54 +0200 Subject: Pogojig mostly done: KiCAD export works --- Makefile | 41 +- readme.md | 219 ----------- src/jig.scad | 2 +- support/asymptote/__init__.py | 0 support/asymptote/__main__.py | 63 --- support/generate_kicad.py | 322 +++++++++++++++ support/inkscape/__init__.py | 0 support/inkscape/__main__.py | 64 --- support/inkscape/bezmisc.py | 273 ------------- support/inkscape/cspsubdiv.py | 37 -- support/inkscape/cubicsuperpath.py | 174 --------- support/inkscape/dxf_footer.txt | 62 --- support/inkscape/dxf_header.txt | 580 ---------------------------- support/inkscape/effect.py | 280 -------------- support/inkscape/ffgeom.py | 141 ------- support/inkscape/inkex.py | 370 ------------------ support/inkscape/inkscape.py | 136 ------- support/inkscape/simplepath.py | 212 ---------- support/inkscape/simpletransform.py | 241 ------------ support/inkscape_exporter.py | 61 +++ support/lib/__init__.py | 0 support/lib/make.py | 5 - support/lib/util.py | 130 ------- support/openscad/__init__.py | 0 support/openscad/__main__.py | 46 --- support/pogojig/__init__.py | 0 support/pogojig/inkscape/__init__.py | 0 support/pogojig/inkscape/bezmisc.py | 287 ++++++++++++++ support/pogojig/inkscape/cspsubdiv.py | 38 ++ support/pogojig/inkscape/cubicsuperpath.py | 174 +++++++++ support/pogojig/inkscape/dxf_footer.txt | 62 +++ support/pogojig/inkscape/dxf_header.txt | 580 ++++++++++++++++++++++++++++ support/pogojig/inkscape/effect.py | 279 +++++++++++++ support/pogojig/inkscape/ffgeom.py | 137 +++++++ support/pogojig/inkscape/inkex.py | 363 +++++++++++++++++ support/pogojig/inkscape/inkscape.py | 124 ++++++ support/pogojig/inkscape/simplepath.py | 213 ++++++++++ support/pogojig/inkscape/simpletransform.py | 242 ++++++++++++ support/pogojig/kicad/__init__.py | 0 support/pogojig/kicad/kicad-cache.lib | 21 + support/pogojig/kicad/kicad.pro | 34 ++ 41 files changed, 2952 insertions(+), 3061 deletions(-) delete mode 100644 readme.md delete mode 100644 support/asymptote/__init__.py delete mode 100644 support/asymptote/__main__.py create mode 100755 support/generate_kicad.py delete mode 100644 support/inkscape/__init__.py delete mode 100644 support/inkscape/__main__.py delete mode 100755 support/inkscape/bezmisc.py delete mode 100755 support/inkscape/cspsubdiv.py delete mode 100755 support/inkscape/cubicsuperpath.py delete mode 100644 support/inkscape/dxf_footer.txt delete mode 100644 support/inkscape/dxf_header.txt delete mode 100644 support/inkscape/effect.py delete mode 100755 support/inkscape/ffgeom.py delete mode 100755 support/inkscape/inkex.py delete mode 100644 support/inkscape/inkscape.py delete mode 100755 support/inkscape/simplepath.py delete mode 100755 support/inkscape/simpletransform.py create mode 100755 support/inkscape_exporter.py delete mode 100644 support/lib/__init__.py delete mode 100644 support/lib/make.py delete mode 100644 support/lib/util.py delete mode 100644 support/openscad/__init__.py delete mode 100644 support/openscad/__main__.py create mode 100644 support/pogojig/__init__.py create mode 100644 support/pogojig/inkscape/__init__.py create mode 100644 support/pogojig/inkscape/bezmisc.py create mode 100644 support/pogojig/inkscape/cspsubdiv.py create mode 100644 support/pogojig/inkscape/cubicsuperpath.py create mode 100644 support/pogojig/inkscape/dxf_footer.txt create mode 100644 support/pogojig/inkscape/dxf_header.txt create mode 100644 support/pogojig/inkscape/effect.py create mode 100644 support/pogojig/inkscape/ffgeom.py create mode 100644 support/pogojig/inkscape/inkex.py create mode 100644 support/pogojig/inkscape/inkscape.py create mode 100644 support/pogojig/inkscape/simplepath.py create mode 100644 support/pogojig/inkscape/simpletransform.py create mode 100644 support/pogojig/kicad/__init__.py create mode 100644 support/pogojig/kicad/kicad-cache.lib create mode 100644 support/pogojig/kicad/kicad.pro diff --git a/Makefile b/Makefile index 26672df..4b66b0d 100644 --- a/Makefile +++ b/Makefile @@ -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 -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) $< $@ + support/inkscape_exporter.py $< $@ src/pcb_shape.dxf: src/pcb_shape.scad src/input.preprocessed.dxf - $(OPENSCAD_CMD) $< $@ + $(OPENSCAD) -o $@ $< src/jig.stl: src/jig.scad src/input.preprocessed.dxf - $(OPENSCAD_CMD) $< $@ + $(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 + diff --git a/readme.md b/readme.md deleted file mode 100644 index fe37316..0000000 --- a/readme.md +++ /dev/null @@ -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 - use - -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. diff --git a/src/jig.scad b/src/jig.scad index 67580bf..d953f29 100644 --- a/src/jig.scad +++ b/src/jig.scad @@ -1,4 +1,4 @@ include <_settings.scad> include <_lib.scad> -jig(height, depth, wall, tolerance, chamfer); +jig(height, depth, wall, tolerance, chamfer); \ No newline at end of file diff --git a/support/asymptote/__init__.py b/support/asymptote/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/support/asymptote/__main__.py b/support/asymptote/__main__.py deleted file mode 100644 index 77bbdc7..0000000 --- a/support/asymptote/__main__.py +++ /dev/null @@ -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) diff --git a/support/generate_kicad.py b/support/generate_kicad.py new file mode 100755 index 0000000..77f58fa --- /dev/null +++ b/support/generate_kicad.py @@ -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')) + diff --git a/support/inkscape/__init__.py b/support/inkscape/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/support/inkscape/__main__.py b/support/inkscape/__main__.py deleted file mode 100644 index 29ac745..0000000 --- a/support/inkscape/__main__.py +++ /dev/null @@ -1,64 +0,0 @@ -import os, shutil -from lib import util -from . import effect, inkscape - - -def _unfuck_svg_document(temp_svg_path): - """ - Unfucks an SVG document so is can be processed by the better_dxf_export - plugin (or what's left of it). - """ - command_line = inkscape.InkscapeCommandLine(temp_svg_path) - layers = command_line.layers - - command_line.apply_to_document('LayerUnlockAll', 'LayerShowAll') - - layer_copies = [] - - for i in layers: - layer_copy = command_line.duplicate_layer(i) - layer_copies.append(layer_copy) - - command_line.apply_to_layer_content(layer_copy, 'ObjectToPath') - command_line.apply_to_layer_content(layer_copy, 'SelectionUnGroup') - - if not i.use_paths: - command_line.apply_to_layer_content(layer_copy, 'StrokeToPath') - command_line.apply_to_layer_content(layer_copy, 'SelectionUnion') - - for original, copy in zip(layers, layer_copies): - command_line.clear_layer(original) - command_line.move_content(copy, original) - 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) - - effect.ExportEffect.check_document_units(in_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) diff --git a/support/inkscape/bezmisc.py b/support/inkscape/bezmisc.py deleted file mode 100755 index b7f5429..0000000 --- a/support/inkscape/bezmisc.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env python -''' -Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru -Copyright (C) 2005 Aaron Spike, aaron@ekips.org - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -''' - -import math, cmath - -def rootWrapper(a,b,c,d): - if a: - # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots - a,b,c = (b/a, c/a, d/a) - m = 2.0*a**3 - 9.0*a*b + 27.0*c - k = a**2 - 3.0*b - n = m**2 - 4.0*k**3 - w1 = -.5 + .5*cmath.sqrt(-3.0) - w2 = -.5 - .5*cmath.sqrt(-3.0) - if n < 0: - m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) - n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) - else: - if m+math.sqrt(n) < 0: - m1 = -pow(-(m+math.sqrt(n))/2,1./3) - else: - m1 = pow((m+math.sqrt(n))/2,1./3) - if m-math.sqrt(n) < 0: - n1 = -pow(-(m-math.sqrt(n))/2,1./3) - else: - n1 = pow((m-math.sqrt(n))/2,1./3) - x1 = -1./3 * (a + m1 + n1) - x2 = -1./3 * (a + w1*m1 + w2*n1) - x3 = -1./3 * (a + w2*m1 + w1*n1) - return (x1,x2,x3) - elif b: - det=c**2.0-4.0*b*d - if det: - return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b) - else: - return -c/(2.0*b), - elif c: - return 1.0*(-d/c), - return () - -def bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))): - #parametric bezier - x0=bx0 - y0=by0 - cx=3*(bx1-x0) - bx=3*(bx2-bx1)-cx - ax=bx3-x0-cx-bx - cy=3*(by1-y0) - by=3*(by2-by1)-cy - ay=by3-y0-cy-by - - 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))): - #parametric line - dd=lx1 - cc=lx2-lx1 - bb=ly1 - aa=ly2-ly1 - - if aa: - coef1=cc/aa - coef2=1 - else: - coef1=1 - coef2=aa/cc - - ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) - #cubic intersection coefficients - a=coef1*ay-coef2*ax - b=coef1*by-coef2*bx - c=coef1*cy-coef2*cx - d=coef1*(y0-bb)-coef2*(x0-dd) - - roots = rootWrapper(a,b,c,d) - retval = [] - for i in roots: - if type(i) is complex and i.imag==0: - i = i.real - if type(i) is not complex and 0<=i<=1: - 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): - 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): - 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)): - ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) - #quadratic coefficents of slope formula - if dx: - slope = 1.0*(dy/dx) - a=3*ay-3*ax*slope - b=2*by-2*bx*slope - c=cy-cx*slope - elif dy: - slope = 1.0*(dx/dy) - a=3*ax-3*ay*slope - b=2*bx-2*by*slope - c=cx-cy*slope - else: - return [] - - roots = rootWrapper(0,a,b,c) - retval = [] - for i in roots: - if type(i) is complex and i.imag==0: - i = i.real - if type(i) is not complex and 0<=i<=1: - retval.append(i) - return retval - -def tpoint((x1,y1),(x2,y2),t): - return x1+t*(x2-x1),y1+t*(y2-y1) -def beziersplitatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t): - m1=tpoint((bx0,by0),(bx1,by1),t) - m2=tpoint((bx1,by1),(bx2,by2),t) - m3=tpoint((bx2,by2),(bx3,by3),t) - m4=tpoint(m1,m2,t) - m5=tpoint(m2,m3,t) - m=tpoint(m4,m5,t) - - return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3)) - -''' -Approximating the arc length of a bezier curve -according to - -if: - L1 = |P0 P1| +|P1 P2| +|P2 P3| - L0 = |P0 P3| -then: - L = 1/2*L0 + 1/2*L1 - ERR = L1-L0 -ERR approaches 0 as the number of subdivisions (m) increases - 2^-4m - -Reference: -Jens Gravesen -"Adaptive subdivision and the length of Bezier curves" -mat-report no. 1992-10, Mathematical Institute, The Technical -University of Denmark. -''' -def pointdistance((x1,y1),(x2,y2)): - return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) -def Gravesen_addifclose(b, len, error = 0.001): - box = 0 - for i in range(1,4): - box += pointdistance(b[i-1], b[i]) - chord = pointdistance(b[0], b[3]) - if (box - chord) > error: - first, second = beziersplitatt(b, 0.5) - Gravesen_addifclose(first, len, error) - Gravesen_addifclose(second, len, error) - else: - len[0] += (box / 2.0) + (chord / 2.0) -def bezierlengthGravesen(b, error = 0.001): - len = [0] - Gravesen_addifclose(b, len, error) - return len[0] - -# balf = Bezier Arc Length Function -balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0 -def balf(t): - retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2 - return math.sqrt(retval) - -def Simpson(f, a, b, n_limit, tolerance): - n = 2 - multiplier = (b - a)/6.0 - endsum = f(a) + f(b) - interval = (b - a)/2.0 - asum = 0.0 - bsum = f(a + interval) - est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) - est0 = 2.0 * est1 - #print multiplier, endsum, interval, asum, bsum, est1, est0 - while n < n_limit and abs(est1 - est0) > tolerance: - n *= 2 - multiplier /= 2.0 - interval /= 2.0 - asum += bsum - bsum = 0.0 - est0 = est1 - for i in xrange(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): - 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): - 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 - t = 1.0 - tdiv = t - curlen = Simpson(balf, 0.0, t, 4096, tolerance) - targetlen = l * curlen - diff = curlen - targetlen - while abs(diff) > tolerance: - tdiv /= 2.0 - if diff < 0: - t += tdiv - else: - t -= tdiv - curlen = Simpson(balf, 0.0, t, 4096, tolerance) - diff = curlen - targetlen - return t - -#default bezier length method -bezierlength = bezierlengthSimpson - -if __name__ == '__main__': - #print linebezierintersect(((,),(,)),((,),(,),(,),(,))) - #print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0))) - tol = 0.00000001 - curves = [((0,0),(1,5),(4,5),(5,5)), - ((0,0),(0,0),(5,0),(10,0)), - ((0,0),(0,0),(5,1),(10,0)), - ((-10,0),(0,0),(10,0),(10,10)), - ((15,10),(0,0),(10,0),(-5,10))] - ''' - for curve in curves: - timing.start() - g = bezierlengthGravesen(curve,tol) - timing.finish() - gt = timing.micro() - - timing.start() - s = bezierlengthSimpson(curve,tol) - timing.finish() - st = timing.micro() - - print g, gt - print s, st - ''' - for curve in curves: - print beziertatlength(curve,0.5) - - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape/cspsubdiv.py b/support/inkscape/cspsubdiv.py deleted file mode 100755 index c34236a..0000000 --- a/support/inkscape/cspsubdiv.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -from bezmisc import * -from ffgeom import * - -def maxdist(((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y))): - p0 = Point(p0x,p0y) - p1 = Point(p1x,p1y) - p2 = Point(p2x,p2y) - p3 = Point(p3x,p3y) - - s1 = Segment(p0,p3) - return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2)) - - -def cspsubdiv(csp,flat): - for sp in csp: - subdiv(sp,flat) - -def subdiv(sp,flat,i=1): - while i < len(sp): - p0 = sp[i-1][1] - p1 = sp[i-1][2] - p2 = sp[i][0] - p3 = sp[i][1] - - b = (p0,p1,p2,p3) - m = maxdist(b) - if m <= flat: - i += 1 - else: - one, two = beziersplitatt(b,0.5) - sp[i-1][2] = one[1] - sp[i][0] = two[2] - p = [one[2],one[3],two[1]] - sp[i:1] = [p] - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape/cubicsuperpath.py b/support/inkscape/cubicsuperpath.py deleted file mode 100755 index 861b9da..0000000 --- a/support/inkscape/cubicsuperpath.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python -""" -cubicsuperpath.py - -Copyright (C) 2005 Aaron Spike, aaron@ekips.org - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -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 math import * - -def matprod(mlist): - prod=mlist[0] - for m in mlist[1:]: - a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0] - a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1] - a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0] - a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1] - prod=[[a00,a01],[a10,a11]] - return prod -def rotmat(teta): - return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]] -def applymat(mat, pt): - x=mat[0][0]*pt[0]+mat[0][1]*pt[1] - y=mat[1][0]*pt[0]+mat[1][1]*pt[1] - pt[0]=x - pt[1]=y -def norm(pt): - return sqrt(pt[0]*pt[0]+pt[1]*pt[1]) - -def ArcToPath(p1,params): - A=p1[:] - rx,ry,teta,longflag,sweepflag,x2,y2=params[:] - teta = teta*pi/180.0 - B=[x2,y2] - if rx==0 or ry==0 or A==B: - return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]]) - mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta))) - applymat(mat, A) - applymat(mat, B) - k=[-(B[1]-A[1]),B[0]-A[0]] - d=k[0]*k[0]+k[1]*k[1] - k[0]/=sqrt(d) - k[1]/=sqrt(d) - d=sqrt(max(0,1-d/4)) - if longflag==sweepflag: - d*=-1 - O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]] - OA=[A[0]-O[0],A[1]-O[1]] - OB=[B[0]-O[0],B[1]-O[1]] - start=acos(OA[0]/norm(OA)) - if OA[1]<0: - start*=-1 - end=acos(OB[0]/norm(OB)) - if OB[1]<0: - end*=-1 - - if sweepflag and start>end: - end +=2*pi - if (not sweepflag) and start> file, code - print >> file, value - - handle_iter = itertools.count(256) - - for layer, path in self._paths: - for (x1, y1), (x2, y2) in zip(path, path[1:]): - write_instruction(0, 'LINE') - - if layer is not None: - write_instruction(8, layer.export_name) - write_instruction(62, layer_indices.get(layer, 0)) - - write_instruction(5, '{:x}'.format(next(handle_iter))) - write_instruction(100, 'AcDbEntity') - write_instruction(100, 'AcDbLine') - write_instruction(10, repr(x1 / unit_factor)) - write_instruction(20, repr(y1 / unit_factor)) - write_instruction(30, 0.0) - write_instruction(11, repr(x2 / unit_factor)) - write_instruction(21, repr(y2 / unit_factor)) - write_instruction(31, 0.0) - - file.write(pkgutil.get_data(__name__, 'dxf_footer.txt')) - - def write_asy(self, file): - def write_line(format, *args): - print >> file, format.format(*args) + ';' - - # Scales pixels to points. Asymptote uses PostScript points (1 / 72 - # inch) by default. - unit_factor = self._unit_factors['pt'] - - paths_by_layer = collections.defaultdict(list) - variable_names = [] - - for layer, path in self._paths: - paths_by_layer[layer].append(path) - - for layer in self._layers + [None]: - paths = paths_by_layer[layer] - variable_name = self._asymptote_identifier_from_layer(layer) - write_line('path[] {}', variable_name) - - variable_names.append(variable_name) - - for path in paths: - point_strs = ['({}, {})'.format(x / unit_factor, y / unit_factor) for x, y in path] - - # Hack. We should determine this from whether Z or z was used - # to close the path in the SVG document. - if path[0] == path[-1]: - point_strs[-1] = 'cycle' - - write_line('{}.push({})', variable_name, ' -- '.join(point_strs)) - - if self._asymptote_all_paths_name not in variable_names: - write_line('path[] {}', self._asymptote_all_paths_name) - - for i in variable_names: - write_line('{}.append({})', self._asymptote_all_paths_name, i) - - @classmethod - def _parse_measure(cls, string): - value_match = re.match(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)', string) - unit_match = re.search('(%s)$' % '|'.join(cls._unit_factors.keys()), string) - - value = float(string[value_match.start():value_match.end()]) - - if unit_match: - unit = string[unit_match.start():unit_match.end()] - else: - unit = None - - return value, unit - - @classmethod - def _measure_to_pixels(cls, string): - """ - Parse a string containing a measure and return it's value converted - to pixels. - """ - value, unit = cls._parse_measure(string) - - return value * cls._get_unit_factor(unit) - - @classmethod - def _get_inkscape_layer_name(cls, node): - while node is not None: - layer = node.get(inkex.addNS('label', 'inkscape')) - - if layer is not None: - return layer - - node = node.getparent() - - return None - - @classmethod - def _get_unit_factor(cls, unit): - if unit is None: - return 1 - else: - return cls._unit_factors[unit] - - @classmethod - def _asymptote_identifier_from_layer(cls, layer): - if layer is None: - return '_' - else: - return re.sub('[^a-zA-Z0-9]', '_', layer.export_name) - - @classmethod - def check_document_units(cls, path): - with open(path, 'r') as file: - p = etree.XMLParser(huge_tree = True) - document = etree.parse(file, parser = p) - - height_attr = document.getroot().get('height') - - if height_attr is None: - raise util.UserError( - '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( - '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( - 'SVG document has no viewBox attribute. See ' - 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') diff --git a/support/inkscape/ffgeom.py b/support/inkscape/ffgeom.py deleted file mode 100755 index ef8799b..0000000 --- a/support/inkscape/ffgeom.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python -""" - ffgeom.py - Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org - - This file is part of FretFind 2-D. - - FretFind 2-D is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - FretFind 2-D is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with FretFind 2-D; if not, write to the Free Software - 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 - def __init__(self, x, y): - self.__coordinates = {'x' : float(x), 'y' : float(y)} - def __getitem__(self, key): - return self.__coordinates[key] - def __setitem__(self, key, value): - self.__coordinates[key] = float(value) - def __repr__(self): - return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision)) - def copy(self): - return Point(self['x'],self['y']) - def translate(self, x, y): - self['x'] += x - self['y'] += y - def move(self, x, y): - self['x'] = float(x) - self['y'] = float(y) - -class Segment: - def __init__(self, e0, e1): - self.__endpoints = [e0, e1] - def __getitem__(self, key): - return self.__endpoints[key] - def __setitem__(self, key, value): - self.__endpoints[key] = value - def __repr__(self): - return repr(self.__endpoints) - def copy(self): - return Segment(self[0],self[1]) - def translate(self, x, y): - self[0].translate(x,y) - self[1].translate(x,y) - def move(self,e0,e1): - self[0] = e0 - self[1] = e1 - def delta_x(self): - return self[1]['x'] - self[0]['x'] - def delta_y(self): - return self[1]['y'] - self[0]['y'] - #alias functions - run = delta_x - rise = delta_y - def slope(self): - if self.delta_x() != 0: - return self.delta_x() / self.delta_y() - return NaN - def intercept(self): - if self.delta_x() != 0: - return self[1]['y'] - (self[0]['x'] * self.slope()) - return NaN - def distanceToPoint(self, p): - s2 = Segment(self[0],p) - c1 = dot(s2,self) - if c1 <= 0: - return Segment(p,self[0]).length() - c2 = dot(self,self) - if c2 <= c1: - return Segment(p,self[1]).length() - return self.perpDistanceToPoint(p) - def perpDistanceToPoint(self, p): - len = self.length() - if len == 0: return 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): - return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180 - 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) - 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) - x = self[0]['x'] + (ratio * self.delta_x()) - y = self[0]['y'] + (ratio * self.delta_y()) - return Point(x, y) - def createParallel(self, p): - return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p) - def intersect(self, s): - return intersectSegments(self, s) - -def intersectSegments(s1, s2): - x1 = s1[0]['x'] - x2 = s1[1]['x'] - x3 = s2[0]['x'] - x4 = s2[1]['x'] - - y1 = s1[0]['y'] - y2 = s1[1]['y'] - y3 = s2[0]['y'] - y4 = s2[1]['y'] - - denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1)) - num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3)) - num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3)) - - num = num1 - - if denom != 0: - x = x1 + ((num / denom) * (x2 - x1)) - y = y1 + ((num / denom) * (y2 - y1)) - return Point(x, y) - return Point(NaN, NaN) - -def dot(s1, s2): - return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y() - - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape/inkex.py b/support/inkscape/inkex.py deleted file mode 100755 index 19e860b..0000000 --- a/support/inkscape/inkex.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -inkex.py -A helper module for creating Inkscape extensions - -Copyright (C) 2005,2010 Aaron Spike and contributors - -Contributors: - Aurélio A. Heckert - Bulia Byak - Nicolas Dufour, nicoduf@yahoo.fr - Peter J. R. Moulder - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" -import copy -import gettext -import optparse -import os -import random -import re -import sys -from math import * - -#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', -u'cc' :u'http://creativecommons.org/ns#', -u'ccOLD' :u'http://web.resource.org/cc/', -u'svg' :u'http://www.w3.org/2000/svg', -u'dc' :u'http://purl.org/dc/elements/1.1/', -u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', -u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape', -u'xlink' :u'http://www.w3.org/1999/xlink', -u'xml' :u'http://www.w3.org/XML/1998/namespace' -} - -def localize(): - domain = 'inkscape' - if sys.platform.startswith('win'): - import locale - current_locale, encoding = locale.getdefaultlocale() - os.environ['LANG'] = current_locale - try: - localdir = os.environ['INKSCAPE_LOCALEDIR']; - trans = gettext.translation(domain, localdir, [current_locale], fallback=True) - except KeyError: - trans = gettext.translation(domain, fallback=True) - elif sys.platform.startswith('darwin'): - try: - localdir = os.environ['INKSCAPE_LOCALEDIR']; - trans = gettext.translation(domain, localdir, fallback=True) - except KeyError: - try: - localdir = os.environ['PACKAGE_LOCALE_DIR']; - trans = gettext.translation(domain, localdir, fallback=True) - except KeyError: - trans = gettext.translation(domain, fallback=True) - else: - try: - localdir = os.environ['PACKAGE_LOCALE_DIR']; - trans = gettext.translation(domain, localdir, fallback=True) - except KeyError: - trans = gettext.translation(domain, fallback=True) - #sys.stderr.write(str(localdir) + "\n") - trans.install() - -def debug(what): - sys.stderr.write(str(what) + "\n") - return what - -def errormsg(msg): - """Intended for end-user-visible error messages. - - (Currently just writes to stderr with an appended newline, but could do - something better in future: e.g. could add markup to distinguish error - messages from status messages or debugging output.) - - Note that this should always be combined with translation: - - import inkex - inkex.localize() - ... - inkex.errormsg(_("This extension requires two selected paths.")) - """ - if isinstance(msg, unicode): - sys.stderr.write(msg.encode("UTF-8") + "\n") - else: - sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("UTF-8")) - -def are_near_relative(a, b, eps): - if (a-b <= a*eps) and (a-b >= -a*eps): - return True - 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 - elif str(value).capitalize() == 'False': - return False - else: - raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (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]!='{': - val = "{%s}%s" % (NSS[ns], tag) - return val - -class InkOption(optparse.Option): - TYPES = optparse.Option.TYPES + ("inkbool",) - TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER) - TYPE_CHECKER["inkbool"] = check_inkbool - -class Effect: - """A class for creating Inkscape SVG Effects""" - - def __init__(self, *args, **kwargs): - self.document=None - self.original_document=None - self.ctx=None - self.selected={} - self.doc_ids={} - self.options=None - self.args=None - self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile",option_class=InkOption) - self.OptionParser.add_option("--id", - action="append", type="string", dest="ids", default=[], - help="id attribute of object to manipulate") - - def effect(self): - pass - - def getoptions(self,args=sys.argv[1:]): - """Collect command line arguments""" - self.options, self.args = self.OptionParser.parse_args(args) - - def parse(self, filename=None): - """Parse document in specified file or on stdin""" - - # First try to open the file from the function argument - if filename != None: - try: - stream = open(filename, 'r') - except Exception: - errormsg(_("Unable to open specified file: %s") % filename) - sys.exit() - - # If it wasn't specified, try to open the file specified as - # an object member - elif self.svg_file != None: - try: - stream = open(self.svg_file, 'r') - except Exception: - errormsg(_("Unable to open object member file: %s") % self.svg_file) - sys.exit() - - # Finally, if the filename was not specified anywhere, use - # standard input stream - else: - stream = sys.stdin - - p = etree.XMLParser(huge_tree=True) - self.document = etree.parse(stream, parser=p) - self.original_document = copy.deepcopy(self.document) - stream.close() - - # defines view_center in terms of document units - def getposinlayer(self): - #defaults - self.current_layer = self.document.getroot() - self.view_center = (0.0,0.0) - - layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS) - if layerattr: - layername = layerattr[0] - layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS) - if layer: - self.current_layer = layer[0] - - xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS) - yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS) - if xattr and yattr: - x = self.unittouu( xattr[0] + 'px' ) - y = self.unittouu( yattr[0] + 'px') - doc_height = self.unittouu(self.document.getroot().get('height')) - if x and y: - self.view_center = (float(x), doc_height - float(y)) # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape - - def getselected(self): - """Collect selected nodes""" - for i in self.options.ids: - path = '//*[@id="%s"]' % i - for node in self.document.xpath(path, namespaces=NSS): - self.selected[i] = node - - def getElementById(self, id): - path = '//*[@id="%s"]' % id - el_list = self.document.xpath(path, namespaces=NSS) - if el_list: - return el_list[0] - else: - return None - - def getParentNode(self, node): - for parent in self.document.getiterator(): - if node in parent.getchildren(): - return parent - break - - - def getdocids(self): - docIdNodes = self.document.xpath('//@id', namespaces=NSS) - for m in docIdNodes: - self.doc_ids[m] = 1 - - def getNamedView(self): - return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0] - - def createGuide(self, posX, posY, angle): - atts = { - 'position': str(posX)+','+str(posY), - 'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle))) - } - guide = etree.SubElement( - self.getNamedView(), - addNS('guide','sodipodi'), atts ) - return guide - - def output(self): - """Serialize document into XML on stdout""" - original = etree.tostring(self.original_document) - result = etree.tostring(self.document) - if original != result: - self.document.write(sys.stdout) - - def affect(self, args=sys.argv[1:], output=True): - """Affect an SVG document with a callback effect""" - self.svg_file = args[-1] - self.getoptions(args) - self.parse() - self.getposinlayer() - self.getselected() - self.getdocids() - self.effect() - if output: self.output() - - def uniqueId(self, old_id, make_new_id = True): - new_id = old_id - if make_new_id: - while new_id in self.doc_ids: - new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') - self.doc_ids[new_id] = 1 - return new_id - - def xpathSingle(self, path): - try: - retval = self.document.xpath(path, namespaces=NSS)[0] - except: - errormsg(_("No matching node for expression: %s") % path) - retval = None - return retval - - #a dictionary of unit to user unit conversion factors - __uuconv = {'in':96.0, 'pt':1.33333333333, 'px':1.0, 'mm':3.77952755913, 'cm':37.7952755913, - 'm':3779.52755913, 'km':3779527.55913, 'pc':16.0, 'yd':3456.0 , 'ft':1152.0} - - # Function returns the unit used for the values in SVG. - # For lack of an attribute in SVG that explicitly defines what units are used for SVG coordinates, - # try to calculate the unit from the SVG width and SVG viewbox. - # Defaults to 'px' units. - def getDocumentUnit(self): - svgunit = 'px' #default to pixels - - svgwidth = self.document.getroot().get('width') - viewboxstr = self.document.getroot().get('viewBox') - if viewboxstr: - unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) - param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') - - p = param.match(svgwidth) - u = unitmatch.search(svgwidth) - - width = 100 #default - viewboxwidth = 100 #default - svgwidthunit = 'px' #default assume 'px' unit - if p: - width = float(p.string[p.start():p.end()]) - else: - errormsg(_("SVG Width not set correctly! Assuming width = 100")) - if u: - svgwidthunit = u.string[u.start():u.end()] - - viewboxnumbers = [] - for t in viewboxstr.split(): - try: - viewboxnumbers.append(float(t)) - except ValueError: - pass - if len(viewboxnumbers) == 4: #check for correct number of numbers - viewboxwidth = viewboxnumbers[2] - - svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth - - # try to find the svgunitfactor in the list of units known. If we don't find something, ... - eps = 0.01 #allow 1% error in factor - for key in self.__uuconv: - if are_near_relative(self.__uuconv[key], svgunitfactor, eps): - #found match! - svgunit = key; - - return svgunit - - - def unittouu(self, string): - '''Returns userunits given a string representation of units in another system''' - unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) - param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') - - p = param.match(string) - u = unit.search(string) - if p: - retval = float(p.string[p.start():p.end()]) - else: - retval = 0.0 - if u: - try: - return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()]) - except KeyError: - pass - else: # default assume 'px' unit - return retval / self.__uuconv[self.getDocumentUnit()] - - return retval - - def uutounit(self, val, unit): - return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()]) - - def addDocumentUnit(self, value): - ''' Add document unit when no unit is specified in the string ''' - try: - float(value) - return value + self.getDocumentUnit() - except ValueError: - return value - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape/inkscape.py b/support/inkscape/inkscape.py deleted file mode 100644 index 9fd9528..0000000 --- a/support/inkscape/inkscape.py +++ /dev/null @@ -1,136 +0,0 @@ -import os -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) - - return list(iter_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())) - - -class Layer(object): - def __init__(self, inkscape_name, export_name, use_paths): - self.inkscape_name = inkscape_name - self.export_name = export_name - self.use_paths = use_paths - - -class InkscapeCommandLine(object): - def __init__(self, path): - self._path = path - self._layers = get_inkscape_layers(path) - self._current_layer_index = None - self._verbs = [] - - def apply_to_document(self, *verb): - self._verbs.extend(verb) - - def apply_to_layer(self, layer, *verb): - self._go_to_layer(layer) - self.apply_to_document(*verb) - - def select_all_in_layer(self, layer): - self.apply_to_layer(layer, 'EditSelectAll') - - def apply_to_layer_content(self, layer, *verbs): - self.select_all_in_layer(layer) - self.apply_to_document(*verbs) - - def _go_to_layer(self, layer, with_selection=False): - if self._current_layer_index is None: - # Initialize to a known state. We cannot assume that any layer is - # selected and thus we need as many LayerPrev as we have layers. - self._current_layer_index = len(self._layers) - self._go_to_layer(self._layers[0]) - - target_index = self._layers.index(layer) - - if with_selection: - next_command = 'LayerMoveToNext' - previous_command = 'LayerMoveToPrev' - else: - next_command = 'LayerNext' - previous_command = 'LayerPrev' - - while self._current_layer_index != target_index: - if self._current_layer_index < target_index: - self.apply_to_document(next_command) - self._current_layer_index += 1 - else: - self.apply_to_document(previous_command) - self._current_layer_index -= 1 - - if with_selection: - # When using LayerMoveToNext and LayerMoveToPrev, inkscape does - # not reliably select the next/previous layer. - self._current_layer_index = None - - def duplicate_layer(self, layer): - self.apply_to_layer(layer, 'LayerDuplicate') - - # Inkscape 0.91 places a duplicated layer above (after) the selected - # one and selects the new layer. - new_layer = Layer(layer.inkscape_name + ' copy', layer.export_name, layer.use_paths) - self._layers.insert(self._current_layer_index + 1, new_layer) - - # Whether the original or the new layer is selected after the operation - # fluctuates between Inkscape versions. - self._current_layer_index = None - - return new_layer - - def delete_layer(self, layer): - self.apply_to_layer(layer, 'LayerDelete') - - # Inkscape 0.91 selects the layer above (after) the deleted layer. - del self._layers[self._current_layer_index] - - def clear_layer(self, layer): - self.select_all_in_layer(layer) - self.apply_to_document('EditDelete') - - def move_content(self, source_layer, target_layer): - self.select_all_in_layer(source_layer) - self._go_to_layer(target_layer, True) - - def run(self): - _inkscape(self._path, self._verbs) - - @property - def layers(self): - return list(self._layers) diff --git a/support/inkscape/simplepath.py b/support/inkscape/simplepath.py deleted file mode 100755 index 94ab092..0000000 --- a/support/inkscape/simplepath.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python -""" -simplepath.py -functions for digesting paths into a simple list structure - -Copyright (C) 2005 Aaron Spike, aaron@ekips.org - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -import re, math - -def lexPath(d): - """ - returns and iterator that breaks path data - identifies command and parameter tokens - """ - offset = 0 - length = len(d) - delim = re.compile(r'[ \t\r\n,]+') - command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') - parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') - while 1: - m = delim.match(d, offset) - if m: - offset = m.end() - if offset >= length: - break - m = command.match(d, offset) - if m: - yield [d[offset:m.end()], True] - offset = m.end() - continue - m = parameter.match(d, offset) - if m: - yield [d[offset:m.end()], False] - offset = m.end() - continue - #TODO: create new exception - raise Exception, 'Invalid path data!' -''' -pathdefs = {commandfamily: - [ - implicitnext, - #params, - [casts,cast,cast], - [coord type,x,y,0] - ]} -''' -pathdefs = { - 'M':['L', 2, [float, float], ['x','y']], - 'L':['L', 2, [float, float], ['x','y']], - 'H':['H', 1, [float], ['x']], - 'V':['V', 1, [float], ['y']], - 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], - 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], - 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], - 'T':['T', 2, [float, float], ['x','y']], - '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. - Removes all shorthand notation. - Converts coordinates to absolute. - """ - retval = [] - lexer = lexPath(d) - - pen = (0.0,0.0) - subPathStart = pen - lastControl = pen - lastCommand = '' - - while 1: - try: - token, isCommand = lexer.next() - except StopIteration: - break - params = [] - needParam = True - if isCommand: - if not lastCommand and token.upper() != 'M': - raise Exception, 'Invalid path, must begin with moveto.' - else: - command = token - else: - #command was omited - #use last command's implicit next command - needParam = False - if lastCommand: - if lastCommand.isupper(): - command = pathdefs[lastCommand][0] - else: - command = pathdefs[lastCommand.upper()][0].lower() - else: - raise Exception, 'Invalid path, no initial command.' - numParams = pathdefs[command.upper()][1] - while numParams > 0: - if needParam: - try: - token, isCommand = lexer.next() - if isCommand: - raise Exception, 'Invalid number of parameters' - except StopIteration: - raise Exception, 'Unexpected end of path' - cast = pathdefs[command.upper()][2][-numParams] - param = cast(token) - if command.islower(): - if pathdefs[command.upper()][3][-numParams]=='x': - param += pen[0] - elif pathdefs[command.upper()][3][-numParams]=='y': - param += pen[1] - params.append(param) - needParam = True - numParams -= 1 - #segment is now absolute so - outputCommand = command.upper() - - #Flesh out shortcut notation - if outputCommand in ('H','V'): - if outputCommand == 'H': - params.append(pen[1]) - if outputCommand == 'V': - params.insert(0,pen[0]) - outputCommand = 'L' - if outputCommand in ('S','T'): - params.insert(0,pen[1]+(pen[1]-lastControl[1])) - params.insert(0,pen[0]+(pen[0]-lastControl[0])) - if outputCommand == 'S': - outputCommand = 'C' - if outputCommand == 'T': - outputCommand = 'Q' - - #current values become "last" values - if outputCommand == 'M': - subPathStart = tuple(params[0:2]) - pen = subPathStart - if outputCommand == 'Z': - pen = subPathStart - else: - pen = tuple(params[-2:]) - - if outputCommand in ('Q','C'): - lastControl = tuple(params[-4:-2]) - else: - lastControl = pen - lastCommand = command - - retval.append([outputCommand,params]) - return retval - -def formatPath(a): - """Format SVG path data from an array""" - return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) - -def translatePath(p, x, y): - for cmd,params in p: - defs = pathdefs[cmd] - for i in range(defs[1]): - if defs[3][i] == 'x': - params[i] += x - elif defs[3][i] == 'y': - params[i] += y - -def scalePath(p, x, y): - for cmd,params in p: - defs = pathdefs[cmd] - for i in range(defs[1]): - if defs[3][i] == 'x': - params[i] *= x - elif defs[3][i] == 'y': - params[i] *= y - elif defs[3][i] == 'r': # radius parameter - params[i] *= x - elif defs[3][i] == 's': # sweep-flag parameter - if x*y < 0: - params[i] = 1 - params[i] - elif defs[3][i] == 'a': # x-axis-rotation angle - if y < 0: - params[i] = - params[i] - -def rotatePath(p, a, cx = 0, cy = 0): - if a == 0: - return p - for cmd,params in p: - defs = pathdefs[cmd] - for i in range(defs[1]): - if defs[3][i] == 'x': - x = params[i] - cx - y = params[i + 1] - cy - r = math.sqrt((x**2) + (y**2)) - if r != 0: - theta = math.atan2(y, x) + a - params[i] = (r * math.cos(theta)) + cx - params[i + 1] = (r * math.sin(theta)) + cy - - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape/simpletransform.py b/support/inkscape/simpletransform.py deleted file mode 100755 index 55082ed..0000000 --- a/support/inkscape/simpletransform.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python -''' -Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr -Copyright (C) 2010 Alvin Penner, penner@vaxxine.com - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -barraud@math.univ-lille1.fr - -This code defines several functions to make handling of transform -attribute easier. -''' -import inkex, cubicsuperpath -import math, re - -def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): - if transf=="" or transf==None: - return(mat) - stransf = transf.strip() - result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf) -#-- translate -- - if result.group(1)=="translate": - args=result.group(2).replace(',',' ').split() - dx=float(args[0]) - if len(args)==1: - dy=0.0 - else: - dy=float(args[1]) - matrix=[[1,0,dx],[0,1,dy]] -#-- scale -- - if result.group(1)=="scale": - args=result.group(2).replace(',',' ').split() - sx=float(args[0]) - if len(args)==1: - sy=sx - else: - sy=float(args[1]) - matrix=[[sx,0,0],[0,sy,0]] -#-- rotate -- - if result.group(1)=="rotate": - args=result.group(2).replace(',',' ').split() - a=float(args[0])*math.pi/180 - if len(args)==1: - cx,cy=(0.0,0.0) - else: - cx,cy=map(float,args[1:]) - matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]] - matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]]) -#-- skewX -- - if result.group(1)=="skewX": - a=float(result.group(2))*math.pi/180 - matrix=[[1,math.tan(a),0],[0,1,0]] -#-- skewY -- - if result.group(1)=="skewY": - a=float(result.group(2))*math.pi/180 - matrix=[[1,0,0],[math.tan(a),1,0]] -#-- matrix -- - if result.group(1)=="matrix": - a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split() - matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]] - - matrix=composeTransform(mat,matrix) - if result.end() < len(stransf): - return(parseTransform(stransf[result.end():], matrix)) - else: - return matrix - -def formatTransform(mat): - return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2])) - -def composeTransform(M1,M2): - a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0] - a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1] - a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0] - a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1] - - v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2] - v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2] - return [[a11,a12,v1],[a21,a22,v2]] - -def composeParents(node, mat): - trans = node.get('transform') - if trans: - mat = composeTransform(parseTransform(trans), mat) - if node.getparent().tag == inkex.addNS('g','svg'): - mat = composeParents(node.getparent(), mat) - return mat - -def applyTransformToNode(mat,node): - m=parseTransform(node.get("transform")) - newtransf=formatTransform(composeTransform(mat,m)) - node.set("transform", newtransf) - -def applyTransformToPoint(mat,pt): - x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2] - y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2] - pt[0]=x - pt[1]=y - -def applyTransformToPath(mat,path): - for comp in path: - for ctl in comp: - for pt in ctl: - applyTransformToPoint(mat,pt) - -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' - t = node.get("transform") - if t == None: - return - m = parseTransform(t) - d = node.get('d') - p = cubicsuperpath.parsePath(d) - applyTransformToPath(m,p) - node.set('d', cubicsuperpath.formatPath(p)) - del node.attrib["transform"] - -#################################################################### -##-- Some functions to compute a rough bbox of a given list of objects. -##-- this should be shipped out in an separate file... - -def boxunion(b1,b2): - if b1 is None: - return b2 - elif b2 is None: - return b1 - else: - return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3]))) - -def roughBBox(path): - xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1] - for pathcomp in path: - for ctl in pathcomp: - for pt in ctl: - xmin = min(xmin,pt[0]) - xMax = max(xMax,pt[0]) - ymin = min(ymin,pt[1]) - yMax = max(yMax,pt[1]) - return xmin,xMax,ymin,yMax - -def refinedBBox(path): - xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1] - for pathcomp in path: - for i in range(1, len(pathcomp)): - cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0]) - xmin = min(xmin, cmin) - xMax = max(xMax, cmax) - cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1]) - ymin = min(ymin, cmin) - yMax = max(yMax, cmax) - return xmin,xMax,ymin,yMax - -def cubicExtrema(y0, y1, y2, y3): - cmin = min(y0, y3) - cmax = max(y0, y3) - d1 = y1 - y0 - d2 = y2 - y1 - d3 = y3 - y2 - if (d1 - 2*d2 + d3): - if (d2*d2 > d1*d3): - t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) - if (t > 0) and (t < 1): - y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t - cmin = min(cmin, y) - cmax = max(cmax, y) - t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) - if (t > 0) and (t < 1): - y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t - cmin = min(cmin, y) - cmax = max(cmax, y) - elif (d3 - d1): - t = -d1/(d3 - d1) - if (t > 0) and (t < 1): - y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t - cmin = min(cmin, y) - cmax = max(cmax, y) - return cmin, cmax - -def computeBBox(aList,mat=[[1,0,0],[0,1,0]]): - bbox=None - for node in aList: - m = parseTransform(node.get('transform')) - m = composeTransform(mat,m) - #TODO: text not supported! - d = None - if node.get("d"): - d = node.get('d') - elif node.get('points'): - d = 'M' + node.get('points') - elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]: - d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \ - 'h' + node.get('width') + 'v' + node.get('height') + \ - 'h-' + node.get('width') - elif node.tag in [ inkex.addNS('line','svg'), 'line' ]: - d = 'M' + node.get('x1') + ',' + node.get('y1') + \ - ' ' + node.get('x2') + ',' + node.get('y2') - elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \ - inkex.addNS('ellipse','svg'), 'ellipse' ]: - rx = node.get('r') - if rx is not None: - ry = rx - else: - rx = node.get('rx') - ry = node.get('ry') - cx = float(node.get('cx', '0')) - cy = float(node.get('cy', '0')) - x1 = cx - float(rx) - x2 = cx + float(rx) - d = 'M %f %f ' % (x1, cy) + \ - 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \ - 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy) - - if d is not None: - p = cubicsuperpath.parsePath(d) - applyTransformToPath(m,p) - bbox=boxunion(refinedBBox(p),bbox) - - elif node.tag == inkex.addNS('use','svg') or node.tag=='use': - refid=node.get(inkex.addNS('href','xlink')) - path = '//*[@id="%s"]' % refid[1:] - refnode = node.xpath(path) - bbox=boxunion(computeBBox(refnode,m),bbox) - - bbox=boxunion(computeBBox(node,m),bbox) - return bbox - - -# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/inkscape_exporter.py b/support/inkscape_exporter.py new file mode 100755 index 0000000..4e80c63 --- /dev/null +++ b/support/inkscape_exporter.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import os +import shutil +import tempfile + +from pogojig.inkscape import effect, inkscape + + +def _unfuck_svg_document(temp_svg_path): + """ + Unfucks an SVG document so is can be processed by the better_dxf_export + plugin (or what's left of it). + """ + command_line = inkscape.InkscapeCommandLine(temp_svg_path) + layers = command_line.layers + + command_line.apply_to_document('LayerUnlockAll', 'LayerShowAll') + + layer_copies = [] + + for i in layers: + layer_copy = command_line.duplicate_layer(i) + layer_copies.append(layer_copy) + + command_line.apply_to_layer_content(layer_copy, 'ObjectToPath') + command_line.apply_to_layer_content(layer_copy, 'SelectionUnGroup') + + if not i.use_paths: + command_line.apply_to_layer_content(layer_copy, 'StrokeToPath') + command_line.apply_to_layer_content(layer_copy, 'SelectionUnion') + + for original, copy in zip(layers, layer_copies): + command_line.clear_layer(original) + command_line.move_content(copy, original) + command_line.delete_layer(copy) + + command_line.apply_to_document('FileSave', 'FileClose', 'FileQuit') + command_line.run() + + +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) + + _unfuck_svg_document(temp_svg_path) + + 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) diff --git a/support/lib/__init__.py b/support/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/support/lib/make.py b/support/lib/make.py deleted file mode 100644 index c89b306..0000000 --- a/support/lib/make.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import util - - -def write_dependencies(path, target, dependencies): - util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode()) diff --git a/support/lib/util.py b/support/lib/util.py deleted file mode 100644 index c00a5fe..0000000 --- a/support/lib/util.py +++ /dev/null @@ -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 - ). - """ - 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() diff --git a/support/openscad/__init__.py b/support/openscad/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/support/openscad/__main__.py b/support/openscad/__main__.py deleted file mode 100644 index 85f8b99..0000000 --- a/support/openscad/__main__.py +++ /dev/null @@ -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) diff --git a/support/pogojig/__init__.py b/support/pogojig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/pogojig/inkscape/__init__.py b/support/pogojig/inkscape/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/pogojig/inkscape/bezmisc.py b/support/pogojig/inkscape/bezmisc.py new file mode 100644 index 0000000..68a338d --- /dev/null +++ b/support/pogojig/inkscape/bezmisc.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +''' +Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +''' + +import math, cmath + +def rootWrapper(a,b,c,d): + if a: + # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots + a,b,c = (b/a, c/a, d/a) + m = 2.0*a**3 - 9.0*a*b + 27.0*c + k = a**2 - 3.0*b + n = m**2 - 4.0*k**3 + w1 = -.5 + .5*cmath.sqrt(-3.0) + w2 = -.5 - .5*cmath.sqrt(-3.0) + if n < 0: + m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) + n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) + else: + if m+math.sqrt(n) < 0: + m1 = -pow(-(m+math.sqrt(n))/2,1./3) + else: + m1 = pow((m+math.sqrt(n))/2,1./3) + if m-math.sqrt(n) < 0: + n1 = -pow(-(m-math.sqrt(n))/2,1./3) + else: + n1 = pow((m-math.sqrt(n))/2,1./3) + x1 = -1./3 * (a + m1 + n1) + x2 = -1./3 * (a + w1*m1 + w2*n1) + x3 = -1./3 * (a + w2*m1 + w1*n1) + return (x1,x2,x3) + elif b: + det=c**2.0-4.0*b*d + if det: + return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b) + else: + return -c/(2.0*b), + elif c: + return 1.0*(-d/c), + return () + +def bezierparameterize(points): + #parametric bezier + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = points + x0=bx0 + y0=by0 + cx=3*(bx1-x0) + bx=3*(bx2-bx1)-cx + ax=bx3-x0-cx-bx + cy=3*(by1-y0) + by=3*(by2-by1)-cy + ay=by3-y0-cy-by + + 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(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 + aa=ly2-ly1 + + if aa: + coef1=cc/aa + coef2=1 + else: + coef1=1 + coef2=aa/cc + + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + #cubic intersection coefficients + a=coef1*ay-coef2*ax + b=coef1*by-coef2*bx + c=coef1*cy-coef2*cx + d=coef1*(y0-bb)-coef2*(x0-dd) + + roots = rootWrapper(a,b,c,d) + retval = [] + for i in roots: + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i)) + return retval + +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(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(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: + slope = 1.0*(dy/dx) + a=3*ay-3*ax*slope + b=2*by-2*bx*slope + c=cy-cx*slope + elif dy: + slope = 1.0*(dx/dy) + a=3*ax-3*ay*slope + b=2*bx-2*by*slope + c=cx-cy*slope + else: + return [] + + roots = rootWrapper(0,a,b,c) + retval = [] + for i in roots: + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + retval.append(i) + return retval + +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(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) + m4=tpoint(m1,m2,t) + m5=tpoint(m2,m3,t) + m=tpoint(m4,m5,t) + + return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3)) + +''' +Approximating the arc length of a bezier curve +according to + +if: + L1 = |P0 P1| +|P1 P2| +|P2 P3| + L0 = |P0 P3| +then: + L = 1/2*L0 + 1/2*L1 + ERR = L1-L0 +ERR approaches 0 as the number of subdivisions (m) increases + 2^-4m + +Reference: +Jens Gravesen +"Adaptive subdivision and the length of Bezier curves" +mat-report no. 1992-10, Mathematical Institute, The Technical +University of Denmark. +''' +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 + for i in range(1,4): + box += pointdistance(b[i-1], b[i]) + chord = pointdistance(b[0], b[3]) + if (box - chord) > error: + first, second = beziersplitatt(b, 0.5) + Gravesen_addifclose(first, len, error) + Gravesen_addifclose(second, len, error) + else: + len[0] += (box / 2.0) + (chord / 2.0) +def bezierlengthGravesen(b, error = 0.001): + len = [0] + Gravesen_addifclose(b, len, error) + return len[0] + +# balf = Bezier Arc Length Function +balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0 +def balf(t): + retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2 + return math.sqrt(retval) + +def Simpson(f, a, b, n_limit, tolerance): + n = 2 + multiplier = (b - a)/6.0 + endsum = f(a) + f(b) + interval = (b - a)/2.0 + asum = 0.0 + bsum = f(a + interval) + est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) + est0 = 2.0 * est1 + #print multiplier, endsum, interval, asum, bsum, est1, est0 + while n < n_limit and abs(est1 - est0) > tolerance: + n *= 2 + multiplier /= 2.0 + interval /= 2.0 + asum += bsum + bsum = 0.0 + est0 = est1 + 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(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(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 + t = 1.0 + tdiv = t + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + targetlen = l * curlen + diff = curlen - targetlen + while abs(diff) > tolerance: + tdiv /= 2.0 + if diff < 0: + t += tdiv + else: + t -= tdiv + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + diff = curlen - targetlen + return t + +#default bezier length method +bezierlength = bezierlengthSimpson + +if __name__ == '__main__': + #print linebezierintersect(((,),(,)),((,),(,),(,),(,))) + #print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0))) + tol = 0.00000001 + curves = [((0,0),(1,5),(4,5),(5,5)), + ((0,0),(0,0),(5,0),(10,0)), + ((0,0),(0,0),(5,1),(10,0)), + ((-10,0),(0,0),(10,0),(10,10)), + ((15,10),(0,0),(10,0),(-5,10))] + ''' + for curve in curves: + timing.start() + g = bezierlengthGravesen(curve,tol) + timing.finish() + gt = timing.micro() + + timing.start() + s = bezierlengthSimpson(curve,tol) + timing.finish() + st = timing.micro() + + print g, gt + print s, st + ''' + for curve in curves: + print(beziertatlength(curve,0.5)) + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/inkscape/cspsubdiv.py b/support/pogojig/inkscape/cspsubdiv.py new file mode 100644 index 0000000..72da473 --- /dev/null +++ b/support/pogojig/inkscape/cspsubdiv.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +from .bezmisc import * +from .ffgeom import * + +def maxdist(points): + ((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = points + p0 = Point(p0x,p0y) + p1 = Point(p1x,p1y) + p2 = Point(p2x,p2y) + p3 = Point(p3x,p3y) + + s1 = Segment(p0,p3) + return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2)) + + +def cspsubdiv(csp,flat): + for sp in csp: + subdiv(sp,flat) + +def subdiv(sp,flat,i=1): + while i < len(sp): + p0 = sp[i-1][1] + p1 = sp[i-1][2] + p2 = sp[i][0] + p3 = sp[i][1] + + b = (p0,p1,p2,p3) + m = maxdist(b) + if m <= flat: + i += 1 + else: + one, two = beziersplitatt(b,0.5) + sp[i-1][2] = one[1] + sp[i][0] = two[2] + p = [one[2],one[3],two[1]] + sp[i:1] = [p] + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/inkscape/cubicsuperpath.py b/support/pogojig/inkscape/cubicsuperpath.py new file mode 100644 index 0000000..a594660 --- /dev/null +++ b/support/pogojig/inkscape/cubicsuperpath.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +""" +cubicsuperpath.py + +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +from . import simplepath +from math import * + +def matprod(mlist): + prod=mlist[0] + for m in mlist[1:]: + a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0] + a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1] + a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0] + a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1] + prod=[[a00,a01],[a10,a11]] + return prod +def rotmat(teta): + return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]] +def applymat(mat, pt): + x=mat[0][0]*pt[0]+mat[0][1]*pt[1] + y=mat[1][0]*pt[0]+mat[1][1]*pt[1] + pt[0]=x + pt[1]=y +def norm(pt): + return sqrt(pt[0]*pt[0]+pt[1]*pt[1]) + +def ArcToPath(p1,params): + A=p1[:] + rx,ry,teta,longflag,sweepflag,x2,y2=params[:] + teta = teta*pi/180.0 + B=[x2,y2] + if rx==0 or ry==0 or A==B: + return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]]) + mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta))) + applymat(mat, A) + applymat(mat, B) + k=[-(B[1]-A[1]),B[0]-A[0]] + d=k[0]*k[0]+k[1]*k[1] + k[0]/=sqrt(d) + k[1]/=sqrt(d) + d=sqrt(max(0,1-d/4)) + if longflag==sweepflag: + d*=-1 + O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]] + OA=[A[0]-O[0],A[1]-O[1]] + OB=[B[0]-O[0],B[1]-O[1]] + start=acos(OA[0]/norm(OA)) + if OA[1]<0: + start*=-1 + end=acos(OB[0]/norm(OB)) + if OB[1]<0: + end*=-1 + + if sweepflag and start>end: + end +=2*pi + if (not sweepflag) and start> file, format.format(*args) + ';' + + # Scales pixels to points. Asymptote uses PostScript points (1 / 72 + # inch) by default. + unit_factor = self._unit_factors['pt'] + + paths_by_layer = collections.defaultdict(list) + variable_names = [] + + for layer, path in self._paths: + paths_by_layer[layer].append(path) + + for layer in self._layers + [None]: + paths = paths_by_layer[layer] + variable_name = self._asymptote_identifier_from_layer(layer) + write_line('path[] {}', variable_name) + + variable_names.append(variable_name) + + for path in paths: + point_strs = ['({}, {})'.format(x / unit_factor, y / unit_factor) for x, y in path] + + # Hack. We should determine this from whether Z or z was used + # to close the path in the SVG document. + if path[0] == path[-1]: + point_strs[-1] = 'cycle' + + write_line('{}.push({})', variable_name, ' -- '.join(point_strs)) + + if self._asymptote_all_paths_name not in variable_names: + write_line('path[] {}', self._asymptote_all_paths_name) + + for i in variable_names: + write_line('{}.append({})', self._asymptote_all_paths_name, i) + + @classmethod + def _parse_measure(cls, string): + value_match = re.match(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)', string) + unit_match = re.search('(%s)$' % '|'.join(cls._unit_factors.keys()), string) + + value = float(string[value_match.start():value_match.end()]) + + if unit_match: + unit = string[unit_match.start():unit_match.end()] + else: + unit = None + + return value, unit + + @classmethod + def _measure_to_pixels(cls, string): + """ + Parse a string containing a measure and return it's value converted + to pixels. + """ + value, unit = cls._parse_measure(string) + + return value * cls._get_unit_factor(unit) + + @classmethod + def _get_inkscape_layer_name(cls, node): + while node is not None: + layer = node.get(inkex.addNS('label', 'inkscape')) + + if layer is not None: + return layer + + node = node.getparent() + + return None + + @classmethod + def _get_unit_factor(cls, unit): + if unit is None: + return 1 + else: + return cls._unit_factors[unit] + + @classmethod + def _asymptote_identifier_from_layer(cls, layer): + if layer is None: + return '_' + else: + return re.sub('[^a-zA-Z0-9]', '_', layer.export_name) + + @classmethod + def check_document_units(cls, path): + with open(path, 'r') as file: + p = etree.XMLParser(huge_tree = True) + document = etree.parse(file, parser = p) + + height_attr = document.getroot().get('height') + + if height_attr is None: + 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 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 ValueError( + 'SVG document has no viewBox attribute. See ' + 'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements') diff --git a/support/pogojig/inkscape/ffgeom.py b/support/pogojig/inkscape/ffgeom.py new file mode 100644 index 0000000..368a29a --- /dev/null +++ b/support/pogojig/inkscape/ffgeom.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +""" + ffgeom.py + Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org + + This file is part of FretFind 2-D. + + FretFind 2-D is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + FretFind 2-D is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FretFind 2-D; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +import math + +class Point: + precision = 5 + def __init__(self, x, y): + self.__coordinates = {'x' : float(x), 'y' : float(y)} + def __getitem__(self, key): + return self.__coordinates[key] + def __setitem__(self, key, value): + self.__coordinates[key] = float(value) + def __repr__(self): + return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision)) + def copy(self): + return Point(self['x'],self['y']) + def translate(self, x, y): + self['x'] += x + self['y'] += y + def move(self, x, y): + self['x'] = float(x) + self['y'] = float(y) + +class Segment: + def __init__(self, e0, e1): + self.__endpoints = [e0, e1] + def __getitem__(self, key): + return self.__endpoints[key] + def __setitem__(self, key, value): + self.__endpoints[key] = value + def __repr__(self): + return repr(self.__endpoints) + def copy(self): + return Segment(self[0],self[1]) + def translate(self, x, y): + self[0].translate(x,y) + self[1].translate(x,y) + def move(self,e0,e1): + self[0] = e0 + self[1] = e1 + def delta_x(self): + return self[1]['x'] - self[0]['x'] + def delta_y(self): + return self[1]['y'] - self[0]['y'] + #alias functions + run = delta_x + rise = delta_y + def slope(self): + if self.delta_x() != 0: + return self.delta_x() / self.delta_y() + return math.nan + def intercept(self): + if self.delta_x() != 0: + return self[1]['y'] - (self[0]['x'] * self.slope()) + return math.nan + def distanceToPoint(self, p): + s2 = Segment(self[0],p) + c1 = dot(s2,self) + if c1 <= 0: + return Segment(p,self[0]).length() + c2 = dot(self,self) + if c2 <= c1: + return Segment(p,self[1]).length() + return self.perpDistanceToPoint(p) + def perpDistanceToPoint(self, p): + len = self.length() + 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): + return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180 + def length(self): + return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2)) + def pointAtLength(self, len): + 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(math.nan, math.nan) + x = self[0]['x'] + (ratio * self.delta_x()) + y = self[0]['y'] + (ratio * self.delta_y()) + return Point(x, y) + def createParallel(self, p): + return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p) + def intersect(self, s): + return intersectSegments(self, s) + +def intersectSegments(s1, s2): + x1 = s1[0]['x'] + x2 = s1[1]['x'] + x3 = s2[0]['x'] + x4 = s2[1]['x'] + + y1 = s1[0]['y'] + y2 = s1[1]['y'] + y3 = s2[0]['y'] + y4 = s2[1]['y'] + + denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1)) + num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3)) + num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3)) + + num = num1 + + if denom != 0: + x = x1 + ((num / denom) * (x2 - x1)) + y = y1 + ((num / denom) * (y2 - y1)) + return Point(x, y) + return Point(math.nan, math.nan) + +def dot(s1, s2): + return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y() + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/inkscape/inkex.py b/support/pogojig/inkscape/inkex.py new file mode 100644 index 0000000..609ffeb --- /dev/null +++ b/support/pogojig/inkscape/inkex.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +inkex.py +A helper module for creating Inkscape extensions + +Copyright (C) 2005,2010 Aaron Spike and contributors + +Contributors: + Aurélio A. Heckert + Bulia Byak + Nicolas Dufour, nicoduf@yahoo.fr + Peter J. R. Moulder + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +import copy +import gettext +import optparse +import os +import random +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', +u'cc' :u'http://creativecommons.org/ns#', +u'ccOLD' :u'http://web.resource.org/cc/', +u'svg' :u'http://www.w3.org/2000/svg', +u'dc' :u'http://purl.org/dc/elements/1.1/', +u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', +u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape', +u'xlink' :u'http://www.w3.org/1999/xlink', +u'xml' :u'http://www.w3.org/XML/1998/namespace' +} + +def localize(): + domain = 'inkscape' + if sys.platform.startswith('win'): + import locale + current_locale, encoding = locale.getdefaultlocale() + os.environ['LANG'] = current_locale + try: + localdir = os.environ['INKSCAPE_LOCALEDIR']; + trans = gettext.translation(domain, localdir, [current_locale], fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + elif sys.platform.startswith('darwin'): + try: + localdir = os.environ['INKSCAPE_LOCALEDIR']; + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + try: + localdir = os.environ['PACKAGE_LOCALE_DIR']; + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + else: + try: + localdir = os.environ['PACKAGE_LOCALE_DIR']; + trans = gettext.translation(domain, localdir, fallback=True) + except KeyError: + trans = gettext.translation(domain, fallback=True) + #sys.stderr.write(str(localdir) + "\n") + trans.install() + +def debug(what): + sys.stderr.write(str(what) + "\n") + return what + +def errormsg(msg): + """Intended for end-user-visible error messages. + + (Currently just writes to stderr with an appended newline, but could do + something better in future: e.g. could add markup to distinguish error + messages from status messages or debugging output.) + + Note that this should always be combined with translation: + + import inkex + inkex.localize() + ... + inkex.errormsg(_("This extension requires two selected paths.")) + """ + if isinstance(msg, unicode): + sys.stderr.write(msg.encode("UTF-8") + "\n") + else: + sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("UTF-8")) + +def are_near_relative(a, b, eps): + if (a-b <= a*eps) and (a-b >= -a*eps): + return True + else: + return False + +def check_inkbool(option, opt, value): + if str(value).capitalize() == 'True': + return True + elif str(value).capitalize() == 'False': + return False + else: + raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value)) + +def addNS(tag, ns=None): + val = tag + 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 + +class InkOption(optparse.Option): + TYPES = optparse.Option.TYPES + ("inkbool",) + TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER) + TYPE_CHECKER["inkbool"] = check_inkbool + +class Effect: + """A class for creating Inkscape SVG Effects""" + + def __init__(self, *args, **kwargs): + self.document=None + self.original_document=None + self.ctx=None + self.selected={} + self.doc_ids={} + self.options=None + self.args=None + self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile",option_class=InkOption) + self.OptionParser.add_option("--id", + action="append", type="string", dest="ids", default=[], + help="id attribute of object to manipulate") + + def effect(self): + pass + + def getoptions(self,args=sys.argv[1:]): + """Collect command line arguments""" + self.options, self.args = self.OptionParser.parse_args(args) + + def parse(self, filename=None): + """Parse document in specified file or on stdin""" + + # First try to open the file from the function argument + if filename != None: + try: + stream = open(filename, 'r') + except Exception: + errormsg(_("Unable to open specified file: %s") % filename) + sys.exit() + + # If it wasn't specified, try to open the file specified as + # an object member + elif self.svg_file != None: + try: + stream = open(self.svg_file, 'r') + except Exception: + errormsg(_("Unable to open object member file: %s") % self.svg_file) + sys.exit() + + # Finally, if the filename was not specified anywhere, use + # standard input stream + else: + stream = sys.stdin + + p = etree.XMLParser(huge_tree=True) + self.document = etree.parse(stream, parser=p) + self.original_document = copy.deepcopy(self.document) + stream.close() + + # defines view_center in terms of document units + def getposinlayer(self): + #defaults + self.current_layer = self.document.getroot() + self.view_center = (0.0,0.0) + + layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS) + if layerattr: + layername = layerattr[0] + layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS) + if layer: + self.current_layer = layer[0] + + xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS) + yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS) + if xattr and yattr: + x = self.unittouu( xattr[0] + 'px' ) + y = self.unittouu( yattr[0] + 'px') + doc_height = self.unittouu(self.document.getroot().get('height')) + if x and y: + self.view_center = (float(x), doc_height - float(y)) # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape + + def getselected(self): + """Collect selected nodes""" + for i in self.options.ids: + path = '//*[@id="%s"]' % i + for node in self.document.xpath(path, namespaces=NSS): + self.selected[i] = node + + def getElementById(self, id): + path = '//*[@id="%s"]' % id + el_list = self.document.xpath(path, namespaces=NSS) + if el_list: + return el_list[0] + else: + return None + + def getParentNode(self, node): + for parent in self.document.getiterator(): + if node in parent.getchildren(): + return parent + break + + + def getdocids(self): + docIdNodes = self.document.xpath('//@id', namespaces=NSS) + for m in docIdNodes: + self.doc_ids[m] = 1 + + def getNamedView(self): + return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0] + + def createGuide(self, posX, posY, angle): + atts = { + 'position': str(posX)+','+str(posY), + 'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle))) + } + guide = etree.SubElement( + self.getNamedView(), + addNS('guide','sodipodi'), atts ) + return guide + + def output(self): + """Serialize document into XML on stdout""" + original = etree.tostring(self.original_document) + result = etree.tostring(self.document) + if original != result: + self.document.write(sys.stdout) + + def affect(self, args=sys.argv[1:], output=True): + """Affect an SVG document with a callback effect""" + self.svg_file = args[-1] + self.getoptions(args) + self.parse() + self.getposinlayer() + self.getselected() + self.getdocids() + self.effect() + if output: self.output() + + def uniqueId(self, old_id, make_new_id = True): + new_id = old_id + if make_new_id: + while new_id in self.doc_ids: + new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + self.doc_ids[new_id] = 1 + return new_id + + def xpathSingle(self, path): + try: + retval = self.document.xpath(path, namespaces=NSS)[0] + except: + errormsg(_("No matching node for expression: %s") % path) + retval = None + return retval + + #a dictionary of unit to user unit conversion factors + __uuconv = {'in':96.0, 'pt':1.33333333333, 'px':1.0, 'mm':3.77952755913, 'cm':37.7952755913, + 'm':3779.52755913, 'km':3779527.55913, 'pc':16.0, 'yd':3456.0 , 'ft':1152.0} + + # Function returns the unit used for the values in SVG. + # For lack of an attribute in SVG that explicitly defines what units are used for SVG coordinates, + # try to calculate the unit from the SVG width and SVG viewbox. + # Defaults to 'px' units. + def getDocumentUnit(self): + svgunit = 'px' #default to pixels + + svgwidth = self.document.getroot().get('width') + viewboxstr = self.document.getroot().get('viewBox') + if viewboxstr: + unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + p = param.match(svgwidth) + u = unitmatch.search(svgwidth) + + width = 100 #default + viewboxwidth = 100 #default + svgwidthunit = 'px' #default assume 'px' unit + if p: + width = float(p.string[p.start():p.end()]) + else: + errormsg(_("SVG Width not set correctly! Assuming width = 100")) + if u: + svgwidthunit = u.string[u.start():u.end()] + + viewboxnumbers = [] + for t in viewboxstr.split(): + try: + viewboxnumbers.append(float(t)) + except ValueError: + pass + if len(viewboxnumbers) == 4: #check for correct number of numbers + viewboxwidth = viewboxnumbers[2] + + svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth + + # try to find the svgunitfactor in the list of units known. If we don't find something, ... + eps = 0.01 #allow 1% error in factor + for key in self.__uuconv: + if are_near_relative(self.__uuconv[key], svgunitfactor, eps): + #found match! + svgunit = key; + + return svgunit + + + def unittouu(self, string): + '''Returns userunits given a string representation of units in another system''' + unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys())) + param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + + p = param.match(string) + u = unit.search(string) + if p: + retval = float(p.string[p.start():p.end()]) + else: + retval = 0.0 + if u: + try: + return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()]) + except KeyError: + pass + else: # default assume 'px' unit + return retval / self.__uuconv[self.getDocumentUnit()] + + return retval + + def uutounit(self, val, unit): + return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()]) + + def addDocumentUnit(self, value): + ''' Add document unit when no unit is specified in the string ''' + try: + float(value) + return value + self.getDocumentUnit() + except ValueError: + return value + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/inkscape/inkscape.py b/support/pogojig/inkscape/inkscape.py new file mode 100644 index 0000000..efe7677 --- /dev/null +++ b/support/pogojig/inkscape/inkscape.py @@ -0,0 +1,124 @@ +import os +import subprocess +import xml.etree.ElementTree as etree + +def get_inkscape_layers(svg_path): + document = etree.parse(svg_path) + + 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 + + layers.append(Layer(inkscape_name, export_name, use_paths)) + return layers + + +def _inkscape(svg_path, verbs): + subprocess.run([os.environ.get('INKSCAPE', 'inkscape'), *(x for verb in verbs for x in ('--verb', verb)), svg_path]) + + +class Layer(object): + def __init__(self, inkscape_name, export_name, use_paths): + self.inkscape_name = inkscape_name + self.export_name = export_name + self.use_paths = use_paths + + +class InkscapeCommandLine(object): + def __init__(self, path): + self._path = path + self._layers = get_inkscape_layers(path) + self._current_layer_index = None + self._verbs = [] + + def apply_to_document(self, *verb): + self._verbs.extend(verb) + + def apply_to_layer(self, layer, *verb): + self._go_to_layer(layer) + self.apply_to_document(*verb) + + def select_all_in_layer(self, layer): + self.apply_to_layer(layer, 'EditSelectAll') + + def apply_to_layer_content(self, layer, *verbs): + self.select_all_in_layer(layer) + self.apply_to_document(*verbs) + + def _go_to_layer(self, layer, with_selection=False): + if self._current_layer_index is None: + # Initialize to a known state. We cannot assume that any layer is + # selected and thus we need as many LayerPrev as we have layers. + self._current_layer_index = len(self._layers) + self._go_to_layer(self._layers[0]) + + target_index = self._layers.index(layer) + + if with_selection: + next_command = 'LayerMoveToNext' + previous_command = 'LayerMoveToPrev' + else: + next_command = 'LayerNext' + previous_command = 'LayerPrev' + + while self._current_layer_index != target_index: + if self._current_layer_index < target_index: + self.apply_to_document(next_command) + self._current_layer_index += 1 + else: + self.apply_to_document(previous_command) + self._current_layer_index -= 1 + + if with_selection: + # When using LayerMoveToNext and LayerMoveToPrev, inkscape does + # not reliably select the next/previous layer. + self._current_layer_index = None + + def duplicate_layer(self, layer): + self.apply_to_layer(layer, 'LayerDuplicate') + + # Inkscape 0.91 places a duplicated layer above (after) the selected + # one and selects the new layer. + new_layer = Layer(layer.inkscape_name + ' copy', layer.export_name, layer.use_paths) + self._layers.insert(self._current_layer_index + 1, new_layer) + + # Whether the original or the new layer is selected after the operation + # fluctuates between Inkscape versions. + self._current_layer_index = None + + return new_layer + + def delete_layer(self, layer): + self.apply_to_layer(layer, 'LayerDelete') + + # Inkscape 0.91 selects the layer above (after) the deleted layer. + del self._layers[self._current_layer_index] + + def clear_layer(self, layer): + self.select_all_in_layer(layer) + self.apply_to_document('EditDelete') + + def move_content(self, source_layer, target_layer): + self.select_all_in_layer(source_layer) + self._go_to_layer(target_layer, True) + + def run(self): + _inkscape(self._path, self._verbs) + + @property + def layers(self): + return list(self._layers) diff --git a/support/pogojig/inkscape/simplepath.py b/support/pogojig/inkscape/simplepath.py new file mode 100644 index 0000000..8d4f0a7 --- /dev/null +++ b/support/pogojig/inkscape/simplepath.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +""" +simplepath.py +functions for digesting paths into a simple list structure + +Copyright (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +import re, math + +def lexPath(d): + """ + returns and iterator that breaks path data + identifies command and parameter tokens + """ + offset = 0 + length = len(d) + delim = re.compile(r'[ \t\r\n,]+') + command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') + parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + while 1: + m = delim.match(d, offset) + if m: + offset = m.end() + if offset >= length: + break + m = command.match(d, offset) + if m: + yield [d[offset:m.end()], True] + offset = m.end() + continue + m = parameter.match(d, offset) + if m: + yield [d[offset:m.end()], False] + offset = m.end() + continue + #TODO: create new exception + raise ValueError('Invalid path data!') +''' +pathdefs = {commandfamily: + [ + implicitnext, + #params, + [casts,cast,cast], + [coord type,x,y,0] + ]} +''' +pathdefs = { + 'M':['L', 2, [float, float], ['x','y']], + 'L':['L', 2, [float, float], ['x','y']], + 'H':['H', 1, [float], ['x']], + 'V':['V', 1, [float], ['y']], + 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], + 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], + 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], + 'T':['T', 2, [float, float], ['x','y']], + '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. + Removes all shorthand notation. + Converts coordinates to absolute. + """ + retval = [] + lexer = lexPath(d) + + pen = (0.0,0.0) + subPathStart = pen + lastControl = pen + lastCommand = '' + + while 1: + try: + token, isCommand = next(lexer) + except StopIteration: + break + params = [] + needParam = True + if isCommand: + if not lastCommand and token.upper() != 'M': + raise ValueError('Invalid path, must begin with moveto.') + else: + command = token + else: + #command was omited + #use last command's implicit next command + needParam = False + if lastCommand: + if lastCommand.isupper(): + command = pathdefs[lastCommand][0] + else: + command = pathdefs[lastCommand.upper()][0].lower() + else: + raise ValueError('Invalid path, no initial command.') + numParams = pathdefs[command.upper()][1] + while numParams > 0: + if needParam: + try: + token, isCommand = next(lexer) + if isCommand: + raise ValueError('Invalid number of parameters') + except StopIteration: + raise ValueError('Unexpected end of path') + cast = pathdefs[command.upper()][2][-numParams] + param = cast(token) + if command.islower(): + if pathdefs[command.upper()][3][-numParams]=='x': + param += pen[0] + elif pathdefs[command.upper()][3][-numParams]=='y': + param += pen[1] + params.append(param) + needParam = True + numParams -= 1 + #segment is now absolute so + outputCommand = command.upper() + + #Flesh out shortcut notation + if outputCommand in ('H','V'): + if outputCommand == 'H': + params.append(pen[1]) + if outputCommand == 'V': + params.insert(0,pen[0]) + outputCommand = 'L' + if outputCommand in ('S','T'): + params.insert(0,pen[1]+(pen[1]-lastControl[1])) + params.insert(0,pen[0]+(pen[0]-lastControl[0])) + if outputCommand == 'S': + outputCommand = 'C' + if outputCommand == 'T': + outputCommand = 'Q' + + #current values become "last" values + if outputCommand == 'M': + subPathStart = tuple(params[0:2]) + pen = subPathStart + if outputCommand == 'Z': + pen = subPathStart + else: + pen = tuple(params[-2:]) + + if outputCommand in ('Q','C'): + lastControl = tuple(params[-4:-2]) + else: + lastControl = pen + lastCommand = command + + retval.append([outputCommand,params]) + return retval + +def formatPath(a): + """Format SVG path data from an array""" + return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) + +def translatePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] += x + elif defs[3][i] == 'y': + params[i] += y + +def scalePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] *= x + elif defs[3][i] == 'y': + params[i] *= y + elif defs[3][i] == 'r': # radius parameter + params[i] *= x + elif defs[3][i] == 's': # sweep-flag parameter + if x*y < 0: + params[i] = 1 - params[i] + elif defs[3][i] == 'a': # x-axis-rotation angle + if y < 0: + params[i] = - params[i] + +def rotatePath(p, a, cx = 0, cy = 0): + if a == 0: + return p + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + x = params[i] - cx + y = params[i + 1] - cy + r = math.sqrt((x**2) + (y**2)) + if r != 0: + theta = math.atan2(y, x) + a + params[i] = (r * math.cos(theta)) + cx + params[i + 1] = (r * math.sin(theta)) + cy + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/inkscape/simpletransform.py b/support/pogojig/inkscape/simpletransform.py new file mode 100644 index 0000000..610cb57 --- /dev/null +++ b/support/pogojig/inkscape/simpletransform.py @@ -0,0 +1,242 @@ +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr +Copyright (C) 2010 Alvin Penner, penner@vaxxine.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +barraud@math.univ-lille1.fr + +This code defines several functions to make handling of transform +attribute easier. +''' + +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) + stransf = transf.strip() + result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf) +#-- translate -- + if result.group(1)=="translate": + args=result.group(2).replace(',',' ').split() + dx=float(args[0]) + if len(args)==1: + dy=0.0 + else: + dy=float(args[1]) + matrix=[[1,0,dx],[0,1,dy]] +#-- scale -- + if result.group(1)=="scale": + args=result.group(2).replace(',',' ').split() + sx=float(args[0]) + if len(args)==1: + sy=sx + else: + sy=float(args[1]) + matrix=[[sx,0,0],[0,sy,0]] +#-- rotate -- + if result.group(1)=="rotate": + args=result.group(2).replace(',',' ').split() + a=float(args[0])*math.pi/180 + if len(args)==1: + cx,cy=(0.0,0.0) + else: + cx,cy=map(float,args[1:]) + matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]] + matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]]) +#-- skewX -- + if result.group(1)=="skewX": + a=float(result.group(2))*math.pi/180 + matrix=[[1,math.tan(a),0],[0,1,0]] +#-- skewY -- + if result.group(1)=="skewY": + a=float(result.group(2))*math.pi/180 + matrix=[[1,0,0],[math.tan(a),1,0]] +#-- matrix -- + if result.group(1)=="matrix": + a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split() + matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]] + + matrix=composeTransform(mat,matrix) + if result.end() < len(stransf): + return(parseTransform(stransf[result.end():], matrix)) + else: + return matrix + +def formatTransform(mat): + return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2])) + +def composeTransform(M1,M2): + a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0] + a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1] + a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0] + a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1] + + v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2] + v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2] + return [[a11,a12,v1],[a21,a22,v2]] + +def composeParents(node, mat): + trans = node.get('transform') + if trans: + mat = composeTransform(parseTransform(trans), mat) + if node.getparent().tag == inkex.addNS('g','svg'): + mat = composeParents(node.getparent(), mat) + return mat + +def applyTransformToNode(mat,node): + m=parseTransform(node.get("transform")) + newtransf=formatTransform(composeTransform(mat,m)) + node.set("transform", newtransf) + +def applyTransformToPoint(mat,pt): + x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2] + y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2] + pt[0]=x + pt[1]=y + +def applyTransformToPath(mat,path): + for comp in path: + for ctl in comp: + for pt in ctl: + applyTransformToPoint(mat,pt) + +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') + t = node.get("transform") + if t == None: + return + m = parseTransform(t) + d = node.get('d') + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + node.set('d', cubicsuperpath.formatPath(p)) + del node.attrib["transform"] + +#################################################################### +##-- Some functions to compute a rough bbox of a given list of objects. +##-- this should be shipped out in an separate file... + +def boxunion(b1,b2): + if b1 is None: + return b2 + elif b2 is None: + return b1 + else: + return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3]))) + +def roughBBox(path): + xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1] + for pathcomp in path: + for ctl in pathcomp: + for pt in ctl: + xmin = min(xmin,pt[0]) + xMax = max(xMax,pt[0]) + ymin = min(ymin,pt[1]) + yMax = max(yMax,pt[1]) + return xmin,xMax,ymin,yMax + +def refinedBBox(path): + xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1] + for pathcomp in path: + for i in range(1, len(pathcomp)): + cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0]) + xmin = min(xmin, cmin) + xMax = max(xMax, cmax) + cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1]) + ymin = min(ymin, cmin) + yMax = max(yMax, cmax) + return xmin,xMax,ymin,yMax + +def cubicExtrema(y0, y1, y2, y3): + cmin = min(y0, y3) + cmax = max(y0, y3) + d1 = y1 - y0 + d2 = y2 - y1 + d3 = y3 - y2 + if (d1 - 2*d2 + d3): + if (d2*d2 > d1*d3): + t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + elif (d3 - d1): + t = -d1/(d3 - d1) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + return cmin, cmax + +def computeBBox(aList,mat=[[1,0,0],[0,1,0]]): + bbox=None + for node in aList: + m = parseTransform(node.get('transform')) + m = composeTransform(mat,m) + #TODO: text not supported! + d = None + if node.get("d"): + d = node.get('d') + elif node.get('points'): + d = 'M' + node.get('points') + elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]: + d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \ + 'h' + node.get('width') + 'v' + node.get('height') + \ + 'h-' + node.get('width') + elif node.tag in [ inkex.addNS('line','svg'), 'line' ]: + d = 'M' + node.get('x1') + ',' + node.get('y1') + \ + ' ' + node.get('x2') + ',' + node.get('y2') + elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \ + inkex.addNS('ellipse','svg'), 'ellipse' ]: + rx = node.get('r') + if rx is not None: + ry = rx + else: + rx = node.get('rx') + ry = node.get('ry') + cx = float(node.get('cx', '0')) + cy = float(node.get('cy', '0')) + x1 = cx - float(rx) + x2 = cx + float(rx) + d = 'M %f %f ' % (x1, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy) + + if d is not None: + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + bbox=boxunion(refinedBBox(p),bbox) + + elif node.tag == inkex.addNS('use','svg') or node.tag=='use': + refid=node.get(inkex.addNS('href','xlink')) + path = '//*[@id="%s"]' % refid[1:] + refnode = node.xpath(path) + bbox=boxunion(computeBBox(refnode,m),bbox) + + bbox=boxunion(computeBBox(node,m),bbox) + return bbox + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/support/pogojig/kicad/__init__.py b/support/pogojig/kicad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/pogojig/kicad/kicad-cache.lib b/support/pogojig/kicad/kicad-cache.lib new file mode 100644 index 0000000..a98cd73 --- /dev/null +++ b/support/pogojig/kicad/kicad-cache.lib @@ -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 diff --git a/support/pogojig/kicad/kicad.pro b/support/pogojig/kicad/kicad.pro new file mode 100644 index 0000000..5cd0983 --- /dev/null +++ b/support/pogojig/kicad/kicad.pro @@ -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] -- cgit