summaryrefslogtreecommitdiff
path: root/support
diff options
context:
space:
mode:
Diffstat (limited to 'support')
-rw-r--r--support/asymptote/__main__.py102
-rw-r--r--support/inkscape/__main__.py108
-rw-r--r--support/inkscape/effect.py520
-rw-r--r--support/inkscape/inkscape.py226
-rw-r--r--support/lib/make.py2
-rw-r--r--support/lib/util.py197
-rw-r--r--support/openscad/__main__.py70
7 files changed, 635 insertions, 590 deletions
diff --git a/support/asymptote/__main__.py b/support/asymptote/__main__.py
index 705649a..77bbdc7 100644
--- a/support/asymptote/__main__.py
+++ b/support/asymptote/__main__.py
@@ -3,55 +3,61 @@ 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
+ 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)
+ 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/inkscape/__main__.py b/support/inkscape/__main__.py
index 78ffb55..29ac745 100644
--- a/support/inkscape/__main__.py
+++ b/support/inkscape/__main__.py
@@ -4,61 +4,61 @@ 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()
+ """
+ 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)
+ 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/effect.py b/support/inkscape/effect.py
index 6acabfb..313032c 100644
--- a/support/inkscape/effect.py
+++ b/support/inkscape/effect.py
@@ -1,264 +1,280 @@
"""
-Based on code from Aaron Spike. See http://www.bobcookdev.com/inkscape/inkscape-dxf.html
+Based on code from Aaron Spike. See
+http://www.bobcookdev.com/inkscape/inkscape-dxf.html
"""
-import pkgutil, os, re, collections, itertools
+import collections
+import itertools
+import os
+import pkgutil
+import re
from lxml import etree
+
from lib import util
from . import inkex, simpletransform, cubicsuperpath, cspsubdiv, inkscape
def _get_unit_factors_map():
- # Fluctuates somewhat between Inkscape releases _and_ between SVG version.
- pixels_per_inch = 96.
- pixels_per_mm = pixels_per_inch / 25.4
-
- return {
- 'px': 1.0,
- 'mm': pixels_per_mm,
- 'cm': pixels_per_mm * 10,
- 'm' : pixels_per_mm * 1e3,
- 'km': pixels_per_mm * 1e6,
- 'pt': pixels_per_inch / 72,
- 'pc': pixels_per_inch / 6,
- 'in': pixels_per_inch,
- 'ft': pixels_per_inch * 12,
- 'yd': pixels_per_inch * 36 }
+ # Fluctuates somewhat between Inkscape releases _and_ between SVG version.
+ pixels_per_inch = 96.
+ pixels_per_mm = pixels_per_inch / 25.4
+
+ return {
+ 'px': 1.0,
+ 'mm': pixels_per_mm,
+ 'cm': pixels_per_mm * 10,
+ 'm': pixels_per_mm * 1e3,
+ 'km': pixels_per_mm * 1e6,
+ 'pt': pixels_per_inch / 72,
+ 'pc': pixels_per_inch / 6,
+ 'in': pixels_per_inch,
+ 'ft': pixels_per_inch * 12,
+ 'yd': pixels_per_inch * 36}
class ExportEffect(inkex.Effect):
- _unit_factors = _get_unit_factors_map()
- _asymptote_all_paths_name = 'all'
-
- def __init__(self):
- inkex.Effect.__init__(self)
-
- self._flatness = float(os.environ['DXF_FLATNESS'])
-
- self._layers = None
- self._paths = None
-
- def _get_document_scale(self):
- """
- Return scaling factor applied to the document because of a viewBox setting. This currently ignores any setting of a preserveAspectRatio attribute (like Inkscape).
- """
-
- document_height = self._get_height()
- view_box = self._get_view_box()
-
- if view_box is None or document_height is None:
- return 1
- else:
- _, _, _, view_box_height = view_box
-
- return document_height / view_box_height
-
- def _get_document_height(self):
- """
- Get the height of the document in pixels in the document coordinate system as it is interpreted by Inkscape.
- """
-
- view_box = self._get_view_box()
- document_height = self._get_height()
-
- if view_box is not None:
- _, _, _, view_box_height = view_box
-
- return view_box_height
- elif document_height is not None:
- return document_height
- else:
- return 0
-
- def _get_height(self):
- height_attr = self.document.getroot().get('height')
-
- if height_attr is None:
- return None
- else:
- return self._measure_to_pixels(height_attr)
-
- def _get_view_box(self):
- view_box_attr = self.document.getroot().get('viewBox')
-
- if view_box_attr is None:
- return None
- else:
- return [float(i) for i in view_box_attr.split()]
-
- def _get_shape_paths(self, node, transform):
- shape = cubicsuperpath.parsePath(node.get('d'))
-
- transform = simpletransform.composeTransform(
- transform,
- simpletransform.composeParents(node, [[1, 0, 0], [0, 1, 0]]))
-
- simpletransform.applyTransformToPath(transform, shape)
-
- def iter_paths():
- for path in shape:
- cspsubdiv.subdiv(path, self._flatness)
-
- # path contains two control point coordinates and the actual coordinates per point.
- yield [i for _, i, _ in path]
-
- return list(iter_paths())
-
- def effect(self):
- document_height = self._get_document_height()
- document_scale = self._get_document_scale()
-
- transform = simpletransform.composeTransform(
- [[document_scale, 0, 0], [0, document_scale, 0]],
- [[1, 0, 0], [0, -1, document_height]])
-
- layers = inkscape.get_inkscape_layers(self.svg_file)
- layers_by_inkscape_name = { i.inkscape_name: i for i in layers }
-
- def iter_paths():
- for node in self.document.getroot().xpath('//svg:path', namespaces = inkex.NSS):
- layer = layers_by_inkscape_name.get(self._get_inkscape_layer_name(node))
-
- for path in self._get_shape_paths(node, transform):
- yield layer, path
-
- self._layers = layers
- self._paths = list(iter_paths())
-
- def write_dxf(self, file):
- # Scales pixels to millimeters. This is the predominant unit in CAD.
- unit_factor = self._unit_factors['mm']
-
- layer_indices = { l: i for i, l in enumerate(self._layers) }
-
- file.write(pkgutil.get_data(__name__, 'dxf_header.txt'))
-
- def write_instruction(code, value):
- print >> 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')
+ _unit_factors = _get_unit_factors_map()
+ _asymptote_all_paths_name = 'all'
+
+ def __init__(self):
+ inkex.Effect.__init__(self)
+
+ self._flatness = float(os.environ['DXF_FLATNESS'])
+
+ self._layers = None
+ self._paths = None
+
+ def _get_document_scale(self):
+ """
+ Return scaling factor applied to the document because of a viewBox
+ setting. This currently ignores any setting of a preserveAspectRatio
+ attribute (like Inkscape).
+ """
+ document_height = self._get_height()
+ view_box = self._get_view_box()
+
+ if view_box is None or document_height is None:
+ return 1
+ else:
+ _, _, _, view_box_height = view_box
+
+ return document_height / view_box_height
+
+ def _get_document_height(self):
+ """
+ Get the height of the document in pixels in the document coordinate
+ system as it is interpreted by Inkscape.
+ """
+ view_box = self._get_view_box()
+ document_height = self._get_height()
+
+ if view_box is not None:
+ _, _, _, view_box_height = view_box
+
+ return view_box_height
+ elif document_height is not None:
+ return document_height
+ else:
+ return 0
+
+ def _get_height(self):
+ height_attr = self.document.getroot().get('height')
+
+ if height_attr is None:
+ return None
+ else:
+ return self._measure_to_pixels(height_attr)
+
+ def _get_view_box(self):
+ view_box_attr = self.document.getroot().get('viewBox')
+
+ if view_box_attr is None:
+ return None
+ else:
+ return [float(i) for i in view_box_attr.split()]
+
+ def _get_shape_paths(self, node, transform):
+ shape = cubicsuperpath.parsePath(node.get('d'))
+
+ transform = simpletransform.composeTransform(
+ transform,
+ simpletransform.composeParents(node, [[1, 0, 0], [0, 1, 0]]))
+
+ simpletransform.applyTransformToPath(transform, shape)
+
+ def iter_paths():
+ for path in shape:
+ cspsubdiv.subdiv(path, self._flatness)
+
+ # path contains two control point coordinates and the actual
+ # coordinates per point.
+ yield [i for _, i, _ in path]
+
+ return list(iter_paths())
+
+ def effect(self):
+ document_height = self._get_document_height()
+ document_scale = self._get_document_scale()
+
+ transform = simpletransform.composeTransform(
+ [[document_scale, 0, 0], [0, document_scale, 0]],
+ [[1, 0, 0], [0, -1, document_height]])
+
+ layers = inkscape.get_inkscape_layers(self.svg_file)
+ layers_by_inkscape_name = {i.inkscape_name: i for i in layers}
+
+ def iter_paths():
+ for node in self.document.getroot().xpath('//svg:path', namespaces=inkex.NSS):
+ layer = layers_by_inkscape_name.get(self._get_inkscape_layer_name(node))
+
+ for path in self._get_shape_paths(node, transform):
+ yield layer, path
+
+ self._layers = layers
+ self._paths = list(iter_paths())
+
+ def write_dxf(self, file):
+ # Scales pixels to millimeters. This is the predominant unit in CAD.
+ unit_factor = self._unit_factors['mm']
+
+ layer_indices = {l: i for i, l in enumerate(self._layers)}
+
+ file.write(pkgutil.get_data(__name__, 'dxf_header.txt'))
+
+ def write_instruction(code, value):
+ print >> 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/inkscape.py b/support/inkscape/inkscape.py
index 21634d2..dee1542 100644
--- a/support/inkscape/inkscape.py
+++ b/support/inkscape/inkscape.py
@@ -1,125 +1,131 @@
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())
+ 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()))
+ 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
+ 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 self._current_layer_index < target_index:
- for _ in range(target_index - self._current_layer_index):
- self.apply_to_document('LayerMoveToNext' if with_selection else 'LayerNext')
- elif self._current_layer_index > target_index:
- for _ in range(self._current_layer_index - target_index):
- self.apply_to_document('LayerMoveToPrev' if with_selection else 'LayerPrev')
- else:
- return
-
- if with_selection:
- # When using LayerMoveToNext and LayerMoveToPrev, inkscape does not reliably select the next/previous layer.
- self._current_layer_index = None
- else:
- self._current_layer_index = target_index
-
- 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._current_layer_index += 1
- self._layers.insert(self._current_layer_index, new_layer)
-
- 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)
+ 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 self._current_layer_index < target_index:
+ for _ in range(target_index - self._current_layer_index):
+ self.apply_to_document(
+ 'LayerMoveToNext' if with_selection else 'LayerNext')
+ elif self._current_layer_index > target_index:
+ for _ in range(self._current_layer_index - target_index):
+ self.apply_to_document(
+ 'LayerMoveToPrev' if with_selection else 'LayerPrev')
+ else:
+ return
+
+ if with_selection:
+ # When using LayerMoveToNext and LayerMoveToPrev, inkscape does
+ # not reliably select the next/previous layer.
+ self._current_layer_index = None
+ else:
+ self._current_layer_index = target_index
+
+ 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._current_layer_index += 1
+ self._layers.insert(self._current_layer_index, new_layer)
+
+ 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/lib/make.py b/support/lib/make.py
index 3502ef6..c89b306 100644
--- a/support/lib/make.py
+++ b/support/lib/make.py
@@ -2,4 +2,4 @@ from . import util
def write_dependencies(path, target, dependencies):
- util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode())
+ util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode())
diff --git a/support/lib/util.py b/support/lib/util.py
index 8b38bc2..c00a5fe 100644
--- a/support/lib/util.py
+++ b/support/lib/util.py
@@ -1,117 +1,130 @@
-import sys, contextlib, subprocess, tempfile, shutil, re, os, inspect
+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 __init__(self, message, *args):
+ super(UserError, self).__init__(message.format(*args))
def main(fn):
- """Decorator for "main" functions. Decorates a function that should be called when the containing module is run as a script (e.g. via python -m <module>)."""
-
- frame = inspect.currentframe().f_back
-
- def wrapped_fn(*args, **kwargs):
- try:
- fn(*args, **kwargs)
- except UserError as e:
- print >> sys.stderr, 'Error:', e
- sys.exit(1)
- except KeyboardInterrupt:
- sys.exit(2)
-
- if frame.f_globals['__name__'] == '__main__':
- wrapped_fn(*sys.argv[1:])
-
- # Allow the main function also to be called explicitly
- return wrapped_fn
+ """
+ Decorator for "main" functions. Decorates a function that should be
+ called when the containing module is run as a script (e.g. via python -m
+ <module>).
+ """
+ frame = inspect.currentframe().f_back
+
+ def wrapped_fn(*args, **kwargs):
+ try:
+ fn(*args, **kwargs)
+ except UserError as e:
+ print >> sys.stderr, 'Error:', e
+ sys.exit(1)
+ except KeyboardInterrupt:
+ sys.exit(2)
+
+ if frame.f_globals['__name__'] == '__main__':
+ wrapped_fn(*sys.argv[1:])
+
+ # Allow the main function also to be called explicitly
+ return wrapped_fn
def rename_atomic(source_path, target_path):
- """
- Move the file at source_path to target_path.
-
- If both paths reside on the same device, os.rename() is used, otherwise the file is copied to a temporary name next to target_path and moved from there using os.rename().
- """
-
- source_dir_stat = os.stat(os.path.dirname(source_path))
- target_dir_stat = os.stat(os.path.dirname(target_path))
-
- if source_dir_stat.st_dev == target_dir_stat.st_dev:
- os.rename(source_path, target_path)
- else:
- temp_path = target_path + '~'
-
- shutil.copyfile(source_path, temp_path)
- os.rename(temp_path, target_path)
+ """
+ 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)
+ 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 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))
+ 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)
+ 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()
+ with open(path, 'rb') as file:
+ return file.read()
diff --git a/support/openscad/__main__.py b/support/openscad/__main__.py
index 7aeaa31..85f8b99 100644
--- a/support/openscad/__main__.py
+++ b/support/openscad/__main__.py
@@ -1,42 +1,46 @@
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.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)
+ 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)