diff options
Diffstat (limited to 'svg-flatten/src')
-rw-r--r-- | svg-flatten/src/main.cpp | 27 | ||||
-rw-r--r-- | svg-flatten/src/out_gdsii.cpp | 121 | ||||
-rw-r--r-- | svg-flatten/src/proto-gen.py | 239 | ||||
-rw-r--r-- | svg-flatten/src/svg_doc.cpp | 19 | ||||
-rw-r--r-- | svg-flatten/src/util.cpp | 8 | ||||
-rw-r--r-- | svg-flatten/src/wasi_exception_workaround.cpp | 15 |
6 files changed, 417 insertions, 12 deletions
diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 1d55437..1fe3454 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -137,7 +137,10 @@ int main(int argc, char **argv) { "Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing)", 0}, {"scale", {"--scale"}, - "Scale input svg lengths by this factor (-o gerber only).", + "Scale input SVG by the given factor.", + 1}, + {"gerber_scale", {"--gerber-scale"}, + "Scale Gerber output coordinates by the given factor.", 1}, {"exclude_groups", {"-e", "--exclude-groups"}, "Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.", @@ -233,20 +236,23 @@ int main(int argc, char **argv) { PolygonSink *sink = nullptr; PolygonSink *flattener = nullptr; PolygonSink *dilater = nullptr; + cerr << "Render sink stack:" << endl; if (fmt == "svg") { string dark_color = args["svg_dark_color"] ? args["svg_dark_color"].as<string>() : "#000000"; string clear_color = args["svg_clear_color"] ? args["svg_clear_color"].as<string>() : "#ffffff"; sink = new SimpleSVGOutput(*out_f, only_polys, precision, dark_color, clear_color); + cerr << " * SVG sink " << endl; } else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber" || fmt == "gerber-outline") { outline_mode = fmt == "gerber-outline"; - double scale = args["scale"].as<double>(1.0); - if (scale != 1.0) { - cerr << "Info: Loading scaled input @scale=" << scale << endl; + double gerber_scale = args["scale"].as<double>(1.0); + if (gerber_scale != 1.0) { + cerr << "Info: Scaling gerber output @gerber_scale=" << gerber_scale << endl; } - sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, scale, {0,0}, args["flip_gerber_polarity"]); + sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision, gerber_scale, {0,0}, args["flip_gerber_polarity"]); + cerr << " * Gerber sink " << endl; } else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") { if (!args["sexp_mod_name"]) { @@ -257,6 +263,7 @@ int main(int argc, char **argv) { sink = new KicadSexpOutput(*out_f, args["sexp_mod_name"], sexp_layer, only_polys); force_flatten = true; is_sexp = true; + cerr << " * KiCAD SExp sink " << endl; } else { cerr << "Error: Unknown output format \"" << fmt << "\"" << endl; @@ -268,11 +275,13 @@ int main(int argc, char **argv) { if (args["dilate"]) { dilater = new Dilater(*top_sink, args["dilate"].as<double>()); top_sink = dilater; + cerr << " * Dilater " << endl; } if (args["flatten"] || (force_flatten && !args["no_flatten"])) { flattener = new Flattener(*top_sink); top_sink = flattener; + cerr << " * Flattener " << endl; } /* Because the C++ stdlib is bullshit */ @@ -454,8 +463,14 @@ int main(int argc, char **argv) { SVGDocument doc; //cerr << "Loading temporary file " << frob << endl; + + double scale = args["scale"].as<double>(1.0); + if (scale != 1.0) { + cerr << "Info: Loading scaled input @scale=" << scale << endl; + } + ifstream load_f(frob); - if (!doc.load(load_f)) { + if (!doc.load(load_f, scale)) { cerr << "Error loading input file \"" << in_f_name << "\", exiting." << endl; return EXIT_FAILURE; } diff --git a/svg-flatten/src/out_gdsii.cpp b/svg-flatten/src/out_gdsii.cpp new file mode 100644 index 0000000..fc7760b --- /dev/null +++ b/svg-flatten/src/out_gdsii.cpp @@ -0,0 +1,121 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <cmath> +#include <algorithm> +#include <string> +#include <iostream> +#include <iomanip> +#include <time.h> +#include <gerbolyze.hpp> +#include <svg_import_defs.h> + +using namespace gerbolyze; +using namespace std; + +SimpleGDSIIOutput::SimpleGDSIIOutput(ostream &out, bool only_polys=false, double scale, d2p offset, bool flip_polarity, std::string libname) + : StreamPolygonSink(out, only_polys), + m_offset(offset), + m_scale(scale), + m_flip_pol(flip_polarity), + m_libname(libname) +{ +} + +void SimpleGDSIIOutput::header_impl(d2p origin, d2p size) { + m_offset[0] += origin[0] * m_scale; + m_offset[1] += origin[1] * m_scale; + m_width = (size[0] - origin[0]) * m_scale; + m_height = (size[1] - origin[1]) * m_scale; + + gds_wr16(GDS_HEADER, {600}); + + time_t t = time(NULL); + struct tm; + gmtime_r(&t, &tm); + gds_wr16(GDS_BGNLIB, {tm.tm_year, tm.tm_month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, + tm.tm_year, tm.tm_month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec}); + + gds_wr_str(GDS_LIBNAME, m_libname); +} + +void gds_wr_d(uint16_t tag, double value) { + uint64_t d_ul = reinterpret_cast<uint64_t>(value); + uint64_t sign = !!(casted & (1ULL<<63)); + int exp = (casted >> 52) & 0x7ffULL; + uint64_t mant = (casted & ((1ULL<<52)-1)) | (1ULL<<52); + + int new_exp = (exp - 1023) / 4 + 64; + int exp_mod = (exp + 1) % 4; + uint64_t new_mant = mant * (1<<exp_mod); + + gds_wr16 +} + +SimpleGDSIIOutput& SimpleGDSIIOutput::operator<<(GerberPolarityToken pol) { + assert(pol == GRB_POL_DARK || pol == GRB_POL_CLEAR); + + if (m_outline_mode) { + assert(pol == GRB_POL_DARK); + } + + if ((pol == GRB_POL_DARK) != m_flip_pol) { + m_out << "%LPD*%" << endl; + } else { + m_out << "%LPC*%" << endl; + } + + return *this; +} +SimpleGDSIIOutput& SimpleGDSIIOutput::operator<<(const Polygon &poly) { + if (poly.size() < 3 && !m_outline_mode) { + cerr << "Warning: " << poly.size() << "-element polygon passed to SimpleGerberOutput" << endl; + return *this; + } + + /* NOTE: Clipper and gerber both have different fixed-point scales. We get points in double mm. */ + double x = round((poly[0][0] * m_scale + m_offset[0]) * m_gerber_scale); + double y = round((m_height - poly[0][1] * m_scale + m_offset[1]) * m_gerber_scale); + if (!m_outline_mode) { + m_out << "G36*" << endl; + } + + m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal /* isn't C++ a marvel of engineering? */ << (long long int)x + << "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y + << "D02*" << endl; + m_out << "G01*" << endl; + + for (size_t i=1; i<poly.size(); i++) { + double x = round((poly[i][0] * m_scale + m_offset[0]) * m_gerber_scale); + double y = round((m_height - poly[i][1] * m_scale + m_offset[1]) * m_gerber_scale); + m_out << "X" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)x + << "Y" << setw(m_digits_int + m_digits_frac) << setfill('0') << std::internal << (long long int)y + << "D01*" << endl; + } + + if (!m_outline_mode) { + m_out << "G37*" << endl; + } + + return *this; +} + +void SimpleGDSIIOutput::footer_impl() { + +} + diff --git a/svg-flatten/src/proto-gen.py b/svg-flatten/src/proto-gen.py new file mode 100644 index 0000000..2f45980 --- /dev/null +++ b/svg-flatten/src/proto-gen.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +import re +import textwrap +import ast + +svg_str = lambda content: content if isinstance(content, str) else '\n'.join(str(c) for c in content) + +class Pattern: + def __init__(self, w, h, content): + self.w = w + self.h = h + self.content = content + + @property + def svg_id(self): + return f'pat-{id(self):16x}' + + def __str__(self): + return textwrap.dedent(f''' + <pattern id="{self.svg_id}" viewBox="0,0,{self.w},{self.h}" width="{self.w}" height="{self.h}" patternUnits="userSpaceOnUse"> + {svg_str(self.content)} + </pattern>''') + + def make_rect(x, y, w, h): + return f'<rect x="{x}" y="{y}" w="{w}" h="{h}" fill="url(#{self.svg_id})"/>' + +class CirclePattern(Pattern): + def __init__(self, d, w, h=None): + self.d = d + self.w = w + self.h = h or w + + @property + def content(self): + return f'<circle cx={self.w/2} cy={self.h/2} r={self.d/2}/>' + +make_layer = lambda layer_name, content: \ + f'<g id="g-{layer_name.replace(" ", "-")}" inkscape:label="{layer_name}" inkscape:groupmode="layer">{svg_str(content)}</g>' + +svg_template = textwrap.dedent(''' + <?xml version="1.0" encoding="UTF-8" standalone="no"?> + <svg version="1.1" width="{w}mm" height="{h}mm" viewBox="0 0 {w} {h}" id="svg18" sodipodi:docname="proto.svg" + inkscape:version="1.2 (dc2aedaf03, 2022-05-15)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs id="defs2"> + {defs} + </defs> + <sodipodi:namedview inkscape:current-layer="g-top-copper" id="namedview4" pagecolor="#ffffff" bordercolor="#666666" + borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" inkscape:zoom="2.8291492" + inkscape:cx="157.29111" inkscape:cy="80.943063" inkscape:window-width="1920" inkscape:window-height="1011" + inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" /> + {layers} + </svg> +''') + +class PatternProtoArea: + def __init__(self, pitch_x, pitch_y=None): + self.pitch_x = pitch_x + self.pitch_y = pitch_y or pitch_x + + @property + def pitch(self): + if self.pitch_x != self.pitch_y: + raise ValueError('Pattern has different X and Y pitches') + return self.pitch_x + + def fit_rect(self, x, y, w, h, center=True): + w_fit, h_fit = round(w - (w % self.pitch_x), 6), round(h - (h % self.pitch_y), 6) + + if center: + x = x + (w-w_fit)/2 + y = y + (h-h_fit)/2 + return x, y, w_fit, h_fit + + else: + return x, y, w_fit, h_fit + +class THTProtoAreaCircles: + def __init__(self, pad_dia=2.0, drill=1.0, pitch=2.54, sides='both', plated=True): + super(pitch) + self.pad_dia = pad_dia + self.drill = drill + self.drill_pattern = CirclePattern(self.drill, self.pitch) + self.pad_pattern = CirclePattern(self.pad_dia, self.pitch) + self.patterns = [self.drill_pattern, self.pad_pattern] + self.plated = plated + self.sides = sides + + def generate(self, x, y, w, h, center=True): + x, y, w, h = self.fit_rect(x, y, w, h, center) + drill = 'plated drill' if self.plated else 'nonplated drill' + d = { drill: self.drill_pattern.make_rect(x, y, w, h) } + + if self.sides in ('top', 'both'): + d['top copper'] = self.pad_pattern.make_rect(x, y, w, h) + if self.sides in ('bottom', 'both'): + d['bottom copper'] = self.pad_pattern.make_rect(x, y, w, h) + + return d + +class ProtoBoard: + def __init__(self, desc_str): + pass + +def convert_to_mm(value, unit): + match unit.lower(): + case 'mm': return value + case 'cm': return value*10 + case 'in': return value*25.4 + case 'mil': return value/1000*25.4 + raise ValueError(f'Invalid unit {unit}, allowed units are mm, cm, in, and mil.') + +value_re = re.compile('([0-9]*\.?[0-9]+)(cm|mm|in|mil|%)') +def eval_value(value, total_length=None): + if not isinstance(value, str): + return None + + m = value_re.match(value) + number, unit = m.groups() + if unit == '%': + if total_length is None: + raise ValueError('Percentages are not allowed for this value') + return total_length * float(number) / 100 + return convert_to_mm(float(number), unit) + +class PropLayout: + def __init__(self, content, direction, proportions): + self.content = content + self.direction = direction + self.proportions = proportions + if len(content) != len(proportions): + raise ValueError('proportions and content must have same length') + + def layout(self, length): + out = [ eval_value(value, length) for value in self.proportions ] + total_length = sum(value for value in out if value is not None) + if length - total_length < -1e-6: + raise ValueError(f'Proportions sum to {total_length} mm, which is greater than the available space of {length} mm.') + + leftover = length - total_length + sum_props = sum( (value or 1.0) for value in self.proportions if not isinstance(value, str) ) + return [ (leftover * (value or 1.0) / sum_props if not isinstance(value, str) else calculated) + for value, calculated in zip(self.proportions, out) ] + + def __str__(self): + children = ', '.join( f'{elem}:{width}' for elem, width in zip(self.content, self.proportions)) + return f'PropLayout[{self.direction.upper()}]({children})' + +def _map_expression(node): + match node: + case ast.Name(): + return node.id + + case ast.Constant(): + return node.value + + case ast.BinOp(op=ast.BitOr()) | ast.BinOp(op=ast.BitAnd()): + left_prop = right_prop = None + + left, right = node.left, node.right + + if isinstance(left, ast.BinOp) and isinstance(left.op, ast.MatMult): + left_prop = _map_expression(left.right) + left = left.left + + if isinstance(right, ast.BinOp) and isinstance(right.op, ast.MatMult): + right_prop = _map_expression(right.right) + right = right.left + + direction = 'h' if isinstance(node.op, ast.BitOr) else 'v' + left, right = _map_expression(left), _map_expression(right) + + if isinstance(left, PropLayout) and left.direction == direction and left_prop is None: + left.content.append(right) + left.proportions.append(right_prop) + return left + + elif isinstance(right, PropLayout) and right.direction == direction and right_prop is None: + right.content.insert(0, left) + right.proportions.insert(0, left_prop) + return right + + else: + return PropLayout([left, right], direction, [left_prop, right_prop]) + + case ast.BinOp(op=ast.MatMult()): + raise SyntaxError(f'Unexpected width specification "{ast.unparse(node.right)}"') + + case _: + raise SyntaxError(f'Invalid layout expression "{ast.unparse(node)}"') + +def parse_layout(expr): + ''' Example layout: + + ( tht @ 2in | smd ) @ 50% / tht + ''' + + expr = re.sub(r'\s', '', expr).lower() + expr = re.sub(r'([0-9]*\.?[0-9]+)(mm|cm|in|mil|%)', r'"\1\2"', expr) + expr = expr.replace('/', '&') + try: + expr = ast.parse(expr, mode='eval').body + match expr: + case ast.Name(): + return PropLayout([expr.id], 'h', [None]) + + case ast.BinOp(op=ast.MatMult()): + assert isinstance(expr.right, ast.Constant) + return PropLayout([_map_expression(expr.left)], 'h', [expr.right.value]) + + case _: + return _map_expression(expr) + except SyntaxError as e: + raise SyntaxError('Invalid layout expression') from e + +if __name__ == '__main__': + import sys + for line in [ + 'tht', + 'tht@1mm', + 'tht|tht', + 'tht@1mm|tht', + 'tht|tht|tht', + 'tht@1mm|tht@2mm|tht@3mm', + '(tht@1mm|tht@2mm)|tht@3mm', + 'tht@1mm|(tht@2mm|tht@3mm)', + 'tht@2|tht|tht', + '(tht@1mm|tht|tht@3mm) / tht', + ]: + layout = parse_layout(line) + print(line, '->', layout) + print(' ', layout.layout(100)) + print() + diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index de56fa1..323a12b 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -32,14 +32,14 @@ using namespace gerbolyze; using namespace std; using namespace ClipperLib; -bool gerbolyze::SVGDocument::load(string filename) { +bool gerbolyze::SVGDocument::load(string filename, double scale) { ifstream in_f; in_f.open(filename); - return in_f && load(in_f); + return in_f && load(in_f, scale); } -bool gerbolyze::SVGDocument::load(istream &in) { +bool gerbolyze::SVGDocument::load(istream &in, double scale) { /* Load XML document */ auto res = svg_doc.load(in); if (!res) { @@ -62,8 +62,8 @@ bool gerbolyze::SVGDocument::load(istream &in) { /* usvg resolves all units, but instead of outputting some reasonable absolute length like mm, it converts * everything to px, which depends on usvg's DPI setting (--dpi). */ - page_w_mm = page_w / assumed_usvg_dpi * 25.4; - page_h_mm = page_h / assumed_usvg_dpi * 25.4; + page_w_mm = page_w / assumed_usvg_dpi * 25.4 * scale; + page_h_mm = page_h / assumed_usvg_dpi * 25.4 * scale; if (!(page_w_mm > 0.0 && page_h_mm > 0.0 && page_w_mm < 10e3 && page_h_mm < 10e3)) { cerr << "Warning: Page has zero or negative size, or is larger than 10 x 10 meters! Parsed size: " << page_w << " x " << page_h << " millimeter" << endl; } @@ -256,6 +256,11 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml bool has_fill = fill_color; bool has_stroke = stroke_color && stroke_width > 0.0; + cerr << "processing svg path" << endl; + cerr << " * " << fill_paths.size() << " fill paths" << endl; + cerr << " * " << stroke_closed.size() << " closed strokes" << endl; + cerr << " * " << stroke_open.size() << " open strokes" << endl; + /* In outline mode, identify drills before applying clip */ if (ctx.settings().outline_mode && has_fill && fill_color != GRB_PATTERN_FILL) { /* Polsby-Popper test */ @@ -301,8 +306,10 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml c.AddPaths(ctx.clip(), ptClip, /* closed */ true); c.StrictlySimple(true); + cerr << "clipping " << fill_paths.size() << " paths, got polytree with " << ptree_fill.ChildCount() << " top-level children" << endl; /* fill rules are nonzero since both subject and clip have already been normalized by clipper. */ c.Execute(ctIntersection, ptree_fill, pftNonZero, pftNonZero); + cerr << " > " << ptree_fill.ChildCount() << " clipped fill ptree top-level children" << endl; } /* Call out to pattern tiler for pattern fills. The path becomes the clip here. */ @@ -402,6 +409,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml stroke_clip.AddPaths(stroke_closed, ptSubject, /* closed */ true); stroke_clip.AddPaths(stroke_open, ptSubject, /* closed */ false); stroke_clip.Execute(ctDifference, ptree, pftNonZero, pftNonZero); + cerr << " > " << ptree.ChildCount() << " clipped stroke ptree top-level children" << endl; /* Did any part of the path clip the clip path (which defaults to the document border)? */ bool nothing_clipped = ptree.Total() == 0; @@ -520,6 +528,7 @@ void gerbolyze::SVGDocument::render_to_list(const RenderSettings &rset, vector<p void gerbolyze::SVGDocument::setup_viewport_clip() { /* Set up view port clip path */ Path vb_path; + cerr << "setting up viewport clip at " << vb_x << ", " << vb_y << " with size " << vb_w << ", " << vb_h << endl; for (d2p &p : vector<d2p> { {vb_x, vb_y}, {vb_x+vb_w, vb_y}, diff --git a/svg-flatten/src/util.cpp b/svg-flatten/src/util.cpp index 0597360..1af5fb6 100644 --- a/svg-flatten/src/util.cpp +++ b/svg-flatten/src/util.cpp @@ -34,13 +34,19 @@ int gerbolyze::run_cargo_command(const char *cmd_name, std::vector<std::string> bool found = false; int proc_rc = -1; for (int i=0; i<3; i++) { + std::string envvar_cx; const char *envvar_val; switch (i) { case 0: if ((envvar_val = getenv(envvar)) == NULL) { continue; } else { - cmdline_c[0] = envvar_val; + if (envvar_val[0] == '~') { + envvar_cx = homedir_s + std::string(envvar_val+1); + cmdline_c[0] = envvar_cx.c_str(); + } else { + cmdline_c[0] = envvar_val; + } } break; diff --git a/svg-flatten/src/wasi_exception_workaround.cpp b/svg-flatten/src/wasi_exception_workaround.cpp new file mode 100644 index 0000000..9d20744 --- /dev/null +++ b/svg-flatten/src/wasi_exception_workaround.cpp @@ -0,0 +1,15 @@ + +#include <typeinfo> +#include <cstdlib> +using namespace std; + +void __cxa_allocate_exception(size_t size) { + (void) size; + abort(); +} + +void __cxa_throw(void* thrown_exception, struct std::type_info * tinfo, void (*dest)(void*)) { + (void) thrown_exception, (void) tinfo, (void) dest; + abort(); +} + |