diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b6a3fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[react]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e75d898 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [ + "web/scripts/alpine-3.10.4.js" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "lineWidth": 120, + "indentWidth": 4 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "useLiteralKeys": "info" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} \ No newline at end of file diff --git a/native/.gitignore b/native/.gitignore index dc6b79a..ac61e5b 100644 --- a/native/.gitignore +++ b/native/.gitignore @@ -1,4 +1,5 @@ zig-cache/ +.zig-cache/ zig-out/ *.o *.a diff --git a/native/build.zig b/native/build.zig index ecdf431..93e9ee1 100644 --- a/native/build.zig +++ b/native/build.zig @@ -14,12 +14,12 @@ pub fn build(b: *std.Build) void { }); const libpotrace_flags = .{ "-std=gnu17", "-DHAVE_CONFIG_H" }; libpotrace.linkLibC(); - libpotrace.addIncludePath(.{ .path = "lib/potrace-1.16/src" }); - libpotrace.addIncludePath(.{ .path = "lib/potrace-config" }); - libpotrace.addCSourceFile(.{ .file = .{ .path = "lib/potrace-1.16/src/curve.c" }, .flags = &libpotrace_flags }); - libpotrace.addCSourceFile(.{ .file = .{ .path = "lib/potrace-1.16/src/trace.c" }, .flags = &libpotrace_flags }); - libpotrace.addCSourceFile(.{ .file = .{ .path = "lib/potrace-1.16/src/decompose.c" }, .flags = &libpotrace_flags }); - libpotrace.addCSourceFile(.{ .file = .{ .path = "lib/potrace-1.16/src/potracelib.c" }, .flags = &libpotrace_flags }); + libpotrace.addIncludePath(b.path("lib/potrace-1.16/src")); + libpotrace.addIncludePath(b.path("lib/potrace-config")); + libpotrace.addCSourceFile(.{ .file = b.path("lib/potrace-1.16/src/curve.c"), .flags = &libpotrace_flags }); + libpotrace.addCSourceFile(.{ .file = b.path("lib/potrace-1.16/src/trace.c"), .flags = &libpotrace_flags }); + libpotrace.addCSourceFile(.{ .file = b.path("lib/potrace-1.16/src/decompose.c"), .flags = &libpotrace_flags }); + libpotrace.addCSourceFile(.{ .file = b.path("lib/potrace-1.16/src/potracelib.c"), .flags = &libpotrace_flags }); const libclipper2 = b.addStaticLibrary(.{ .name = "clipper2", @@ -30,54 +30,47 @@ pub fn build(b: *std.Build) void { const libclipper2_flags = .{ "-std=gnu++17", "-fno-exceptions", "-Dthrow=abort" }; libclipper2.linkLibC(); libclipper2.linkSystemLibrary("c++"); - libclipper2.addIncludePath(.{ .path = "lib/clipper2/CPP/Clipper2Lib" }); - libclipper2.addIncludePath(.{ .path = "src" }); + libclipper2.addIncludePath(b.path("lib/clipper2/CPP/Clipper2Lib")); + libclipper2.addIncludePath(b.path("src")); libclipper2.addCSourceFile(.{ - .file = .{ .path = "lib/clipper2/CPP/Clipper2Lib/clipper.engine.cpp" }, + .file = b.path("lib/clipper2/CPP/Clipper2Lib/clipper.engine.cpp"), .flags = &libclipper2_flags, }); libclipper2.addCSourceFile(.{ - .file = .{ .path = "lib/clipper2/CPP/Clipper2Lib/clipper.offset.cpp" }, + .file = b.path("lib/clipper2/CPP/Clipper2Lib/clipper.offset.cpp"), .flags = &libclipper2_flags, }); libclipper2.addCSourceFile(.{ - .file = .{ .path = "src/clipperwrapper.cpp" }, + .file = b.path("src/clipperwrapper.cpp"), .flags = &libclipper2_flags, }); - const libgingerbread = b.addExecutable(.{ - .name = "gingerbread", - .root_source_file = .{ .path = "src/gingerbread.zig" }, - .version = .{ .major = 1, .minor = 0, .patch = 0 }, - .target = target, - .optimize = optimize, - .strip = true - }); + const libgingerbread = b.addExecutable(.{ .name = "gingerbread", .root_source_file = b.path("src/gingerbread.zig"), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .target = target, .optimize = optimize, .strip = true }); libgingerbread.entry = .disabled; libgingerbread.rdynamic = true; libgingerbread.wasi_exec_model = std.builtin.WasiExecModel.reactor; libgingerbread.linkLibC(); libgingerbread.linkLibrary(libpotrace); libgingerbread.linkLibrary(libclipper2); - libgingerbread.addIncludePath(.{ .path = "src" }); - libgingerbread.addIncludePath(.{ .path = "lib/potrace-1.16/src" }); + libgingerbread.addIncludePath(b.path("src")); + libgingerbread.addIncludePath(b.path("lib/potrace-1.16/src")); b.installArtifact(libgingerbread); // const main = b.addTest(.{ // .name = "main", - // .root_source_file = .{ .path = "src/tests.zig" }, + // .root_source_file = b.path("src/tests.zig"), // .target = target, // .optimize = optimize, // .link_libc = true, // }); // main.linkLibrary(libpotrace); // main.linkLibrary(libclipper2); - // main.addIncludePath(.{ .path = "src/" }); - // main.addIncludePath(.{ .path = "lib/potrace-1.16/src" }); - // main.addIncludePath(.{ .path = "lib/potrace-config" }); - // main.addIncludePath(.{ .path = "lib/stb" }); - // main.addCSourceFile(.{ .file = .{ .path = "src/load_image.c" }, .flags = &.{ + // main.addIncludePath(b.path("src/")); + // main.addIncludePath(b.path("lib/potrace-1.16/src")); + // main.addIncludePath(b.path("lib/potrace-config")); + // main.addIncludePath(b.path("lib/stb")); + // main.addCSourceFile(.{ .file = b.path("src/load_image.c"), .flags = &.{ // "-std=gnu17", // } }); diff --git a/native/src/gingerbread.zig b/native/src/gingerbread.zig index e4e341a..e09bb66 100644 --- a/native/src/gingerbread.zig +++ b/native/src/gingerbread.zig @@ -11,7 +11,7 @@ const wasm = @import("wasm.zig"); const a = wasm.allocator; -fn trace(allocator: std.mem.Allocator, layer_name: []const u8, scale_factor: f64, image_pixels: [*]u8, image_width: usize, image_height: usize, writer: anytype) !void { +fn trace(allocator: std.mem.Allocator, layer_name: []const u8, scale_factor: f64, image_pixels: [*]u8, image_width: usize, image_height: usize, writer: anytype, width_mm: f64) !void { var bitmap = try potrace.Bitmap.from_image(allocator, .{ .pixels = image_pixels, .w = image_width, @@ -39,7 +39,7 @@ fn trace(allocator: std.mem.Allocator, layer_name: []const u8, scale_factor: f64 print("Polylist fractured\n", .{}); - try pcb.polylist_to_footprint(polylist, layer_name, scale_factor, writer); + try pcb.polylist_to_footprint(polylist, layer_name, scale_factor, writer, width_mm); } test "trace" { @@ -70,7 +70,7 @@ export fn conversion_start() void { pcb.start_pcb(conversion_buffer.?.writer()) catch @panic("memory"); } -export fn conversion_add_raster_layer(layer: u32, scale_factor: f64, image_pixels: [*]u8, image_width: u32, image_height: u32) void { +export fn conversion_add_raster_layer(layer: u32, scale_factor: f64, image_pixels: [*]u8, image_width: u32, image_height: u32, width_mm: f64) void { const layer_name = switch (layer) { 1 => "F.Cu", 2 => "B.Cu", @@ -81,7 +81,7 @@ export fn conversion_add_raster_layer(layer: u32, scale_factor: f64, image_pixel else => "Unknown", }; - trace(a, layer_name, scale_factor, image_pixels, image_width, image_height, conversion_buffer.?.writer()) catch @panic("memory"); + trace(a, layer_name, scale_factor, image_pixels, image_width, image_height, conversion_buffer.?.writer(), width_mm) catch @panic("memory"); } export fn conversion_start_poly() void { @@ -91,9 +91,21 @@ export fn conversion_start_poly() void { export fn conversion_add_poly_point( x: f64, y: f64, + layer_number: u32, scale_factor: f64, + width_mm: f64, ) void { - pcb.add_xx_poly_point(.{ .x = x, .y = y }, scale_factor, conversion_buffer.?.writer()) catch @panic("memory"); + const layer_name = switch (layer_number) { + 1 => "F.Cu", + 2 => "B.Cu", + 3 => "F.SilkS", + 4 => "B.SilkS", + 5 => "F.Mask", + 6 => "B.Mask", + else => "Unknown", + }; + + pcb.add_xx_poly_point(.{ .x = x, .y = y }, layer_name, scale_factor, conversion_buffer.?.writer(), width_mm) catch @panic("memory"); } export fn conversion_end_poly(layer: u32, width: f32, fill: bool) void { @@ -115,3 +127,7 @@ export fn conversion_finish() wasm.StringResult { pcb.end_pcb(&conversion_buffer.?.writer()) catch @panic("memory"); return wasm.return_string(conversion_buffer.?.toOwnedSlice() catch @panic("memory")); } + +export fn set_mirror_back_layers(val: bool) void { + pcb.mirror_back_layers = val; +} diff --git a/native/src/pcb.zig b/native/src/pcb.zig index 15cefb0..52308b5 100644 --- a/native/src/pcb.zig +++ b/native/src/pcb.zig @@ -6,6 +6,10 @@ const Poly = geometry.Poly; const PolyList = geometry.PolyList; const FauxUUID = @import("fauxuuid.zig").FauxUUID; +fn is_back_layer(layer: []const u8) bool { + return std.ascii.startsWithIgnoreCase(layer, "B."); +} + pub fn start_pcb(writer: anytype) !void { try writer.writeAll("(kicad_pcb (version 20211014) (generator pcbnew)\n"); try writer.writeAll("(layers\n"); @@ -29,31 +33,41 @@ pub fn start_xx_poly(kind: []const u8, writer: anytype) !void { try writer.print(" ({s}_poly\n", .{kind}); try writer.writeAll(" (pts\n"); } +pub var mirror_back_layers: bool = true; + +pub fn add_xx_poly_point(pt: geometry.Point, layer_name: []const u8, scale_factor: f64, writer: anytype, width_mm: f64) !void { + const scaled_x = pt.x * scale_factor; + const scaled_y = pt.y * scale_factor; + + // For back layers, mirror around y=0 then translate back + const final_x = if (is_back_layer(layer_name) and mirror_back_layers) + -scaled_x + width_mm + else + scaled_x; -pub fn add_xx_poly_point(pt: geometry.Point, scale_factor: f64, writer: anytype) !void { - try writer.print(" (xy {d:.3} {d:.3})\n", .{ pt.x * scale_factor, pt.y * scale_factor }); + try writer.print(" (xy {d:.3} {d:.3})\n", .{ final_x, scaled_y }); } -pub fn end_xx_poly(layer: []const u8, width: f64, fill: bool, writer: anytype) !void { +pub fn end_xx_poly(layer_name: []const u8, line_width: f64, fill: bool, writer: anytype) !void { try writer.writeAll(" )\n"); - try writer.print(" (layer \"{s}\")\n", .{layer}); - try writer.print(" (width {d:.3})\n", .{width}); + try writer.print(" (layer \"{s}\")\n", .{layer_name}); + try writer.print(" (width {d:.3})\n", .{line_width}); try writer.print(" (fill {s})\n", .{if (fill) "solid" else "none"}); try writer.print(" (tstamp \"{s}\")\n", .{FauxUUID.init()}); try writer.writeAll(" )\n"); } -pub fn points_to_xx_poly(kind: []const u8, pts: []geometry.Point, scale_factor: f64, layer: []const u8, width: f64, fill: bool, writer: anytype) !void { +pub fn points_to_xx_poly(kind: []const u8, pts: []geometry.Point, scale_factor: f64, layer_name: []const u8, line_width: f64, fill: bool, writer: anytype, width_mm: f64) !void { try start_xx_poly(kind, writer); for (pts) |pt| { - try add_xx_poly_point(pt, scale_factor, writer); + try add_xx_poly_point(pt, layer_name, scale_factor, writer, width_mm); } - try end_xx_poly(layer, width, fill, writer); + try end_xx_poly(layer_name, line_width, fill, writer); } -pub fn polylist_to_footprint(polylist: PolyList, layer: []const u8, scale_factor: f64, writer: anytype) !void { +pub fn polylist_to_footprint(polylist: PolyList, layer: []const u8, scale_factor: f64, writer: anytype, width_mm: f64) !void { try writer.writeAll("(footprint \"Graphics\"\n"); try writer.print(" (layer \"{s}\")\n", .{layer}); try writer.writeAll(" (at 0 0)\n"); @@ -62,7 +76,7 @@ pub fn polylist_to_footprint(polylist: PolyList, layer: []const u8, scale_factor try writer.print(" (tedit \"{s}\")\n", .{FauxUUID.init()}); for (polylist.items) |poly| { - try points_to_xx_poly("fp", poly.outline, scale_factor, layer, 0, true, writer); + try points_to_xx_poly("fp", poly.outline, scale_factor, layer, 0, true, writer, width_mm); } try writer.writeAll(")\n"); diff --git a/web/help.html b/web/help.html index 4974ef9..7b8c52d 100644 --- a/web/help.html +++ b/web/help.html @@ -17,43 +17,61 @@

About Gingerbread

acknowledgements are at the bottom of this page.

Using Gingerbread

-

At the moment, Gingerbread is intended to work with SVGs created in Affinity Designer. You'll need to make sure your design matches what Gingerbread expects.

+

At the moment, Gingerbread is intended to work with SVGs created in Affinity Designer or Inkscape. You'll need to make sure your design matches what Gingerbread expects. + If using Inkscape, saving your file as an Inkscape SVG is necessary to preserve the layer names.

Page settings

First, it's highly recommended to change your page settings to use millimeters and 2540 DPI, as shown here:

You might be wondering why that specific DPI? Well, 2540 DPI happens to be 1000 dots per mm, which helpfully avoids rounding issues when - exporting the design from Affinity and when converting the outline and drills. You can use other DPIs by changing the DPI setting in Gingerbread once your design is loaded. + exporting the design from your vector editor and when converting the outline and drills. You can use other DPIs by changing the DPI setting in Gingerbread once your design is loaded.

On very slim designs, you might be experiencing issues where the drill holes on either the Drills or Edge.Cuts layers become off-centered from their intended placement. In that case it'll help to extend the canvas size inside Affinity to be square. Make sure to select "Anchor on page" to preserve your designs aspect ratio.

Creating an outline

-

The outline should be drawn on a layer named Edge.Cuts in Affinity. Gingerbread handles this layer in a specific way to make sure that there is a 1-to-1 match - between the size and units in Affinity and KiCAD. This approach can't handle as many complex edge cases as the rasterization approach used by the graphic layers, but as +

The outline should be drawn on a layer named Edge.Cuts in your vector editor. Gingerbread handles this layer in a specific way to make sure that there is a 1-to-1 match + between the size and units in the SVG and KiCAD. This approach can't handle as many complex edge cases as the rasterization approach used by the graphic layers, but as long as your paths have been converted to curves it should handle them well. The outline layer can contain multiple curves, with inside curves getting converted to "cut-outs".

Graphics layers

-

Non-transparent areas on layers named F.SilkS, B.SilkS, F.Cu, and B.Cu in Affinity are converted to their respective +

Non-transparent areas on layers named F.SilkS, B.SilkS, F.Cu, and B.Cu in the SVG are converted to their respective layers in KiCAD. Note that F.Mask and B.Mask are "inverted" like they are in KiCAD, meaning that non-transparent areas indicate where to - remove the soldermask- the preview in Gingerbread will shows the mask layers as they would appear on the printed board. + remove the soldermask- the preview in Gingerbread will shows the mask layers as they would appear on the printed board.

Gingerbread converts these layers by rasterizing all the items on each layer to black and white, re-tracing the raster image to polygons, and placing the resulting polygons into KiCAD. While this might seem odd, it works extremely well for a variety of SVGs.

Drills

-

Items on the layer named Drills in Affinity are also handled in a specific way. Gingerbread walks through all of the shapes in that layer and converts +

Items on the layer named Drills in the SVG are also handled in a specific way. Gingerbread walks through all of the shapes in that layer and converts only circles into corresponding non-plated through hole drills in KiCAD. Just as with the board outline, this is done to preserve position and size between - Affinity and KiCAD. + the SVG and KiCAD.

+

Mirror back layers

+

Gingerbread allows you to mirror the B.SilkS, B.Mask, and B.Cu layers so that the it is correctly mirrored in the output. This can be switched + on if you want this behavior.

+ +

Exporting your design

+

Affinity Designer

When exporting you design to an SVG for Gingerbread, click the More button and setup the export parameters as shown below so that "Rasterize" is set to "Nothing", "Export text as curves" is checked, and "Flatten transforms" is checked.

You can save this as a preset to avoid having to change these every time you export.

+

Inkscape

+

Note: Inkscape support is new and relatively untested. Please do try it out, and if you run into problems, + open an issue on Github with info on what went wrong!

+

Select File->Save As (or Save a Copy) and select Inkscape SVG (*.svg) + from the filetype dropdown in the lower right. To ensure fonts and text are preserved as-is, we recommend converting text to paths before saving: +

    +
  1. Select a single text item (doesn't matter which, as long as it's text)
  2. +
  3. Click Edit->Select Same->Object Type or press Shift+Alt+A to select all text objects
  4. +
  5. Convert them all to paths by clicking Path->Object to Path or pressing Shift+Ctrl+C.
  6. +
+

Converting your design

Once the SVG is exported, drag and drop it onto the Gingerbread web page. Once loaded, you should see a preview of your design. Use the options in the right pane to @@ -61,7 +79,7 @@

Converting your design

into KiCAD's PCBNew.

Copyright and acknowledgements

-

Gingerbread is (c) 2022 by Winterbloom LLC & Alethea Katherine Flowers

+

Gingerbread is (c) 2022 by Winterbloom LLC & Alethea Katherine Flowers

Gingerbread is available under the MIT License:

diff --git a/web/index.html b/web/index.html index 29948a3..2fe7e05 100644 --- a/web/index.html +++ b/web/index.html @@ -126,6 +126,13 @@ +
+ +
+