Skip to content

Commit

Permalink
FillPlotter,StrokePlotter: ignore degenerate line_to
Browse files Browse the repository at this point in the history
This fixes the plotters to just drop line_to movements when they refer
to the current point. This should prevent unexpected artifacts that
would occur by plotting things like degenerate joins, etc.

This manifests in stroke more than anything else I think, fill seems
fine, but we've included it there too (in addition to a unit test) for
correctness.
  • Loading branch information
vancluever committed Oct 8, 2024
1 parent f6c5b38 commit 21c4d25
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 21 deletions.
39 changes: 39 additions & 0 deletions spec/041_stroke_noop_lineto.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: 0BSD
// Copyright © 2024 Chris Marchesi

//! Case: Ensure no-op (degenerate) lineto operations are accounted for
//! properly and do not break strokes.
const debug = @import("std").debug;
const mem = @import("std").mem;

const z2d = @import("z2d");

pub const filename = "041_stroke_noop_lineto";

pub fn render(alloc: mem.Allocator, aa_mode: z2d.options.AntiAliasMode) !z2d.Surface {
const width = 18;
const height = 36;
const sfc = try z2d.Surface.init(.image_surface_rgb, alloc, width, height);

var context: z2d.Context = .{
.surface = sfc,
.pattern = .{
.opaque_pattern = .{
.pixel = .{ .rgb = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF } }, // White on black
},
},
.anti_aliasing_mode = aa_mode,
};

var path = z2d.Path.init(alloc);
defer path.deinit();

try path.moveTo(9, 0);
try path.lineTo(9, 9);
try path.lineTo(0, 18);
try path.lineTo(0, 18);

try context.stroke(alloc, path);

return sfc;
}
Binary file added spec/files/041_stroke_noop_lineto_pixelated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added spec/files/041_stroke_noop_lineto_smooth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions spec/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const _037_stroke_join_overlap = @import("037_stroke_join_overlap.zig");
const _038_stroke_zero_length = @import("038_stroke_zero_length.zig");
const _039_stroke_paint_extent_dontclip = @import("039_stroke_paint_extent_dontclip.zig");
const _040_stroke_corner_symmetrical = @import("040_stroke_corner_symmetrical.zig");
const _041_stroke_noop_lineto = @import("041_stroke_noop_lineto.zig");

//////////////////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -98,6 +99,7 @@ pub fn main() !void {
try pathExportRun(alloc, _038_stroke_zero_length);
try pathExportRun(alloc, _039_stroke_paint_extent_dontclip);
try pathExportRun(alloc, _040_stroke_corner_symmetrical);
try pathExportRun(alloc, _041_stroke_noop_lineto);
}

//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -262,6 +264,10 @@ test "040_stroke_corner_symmetrical" {
try pathTestRun(testing.allocator, _040_stroke_corner_symmetrical);
}

test "041_stroke_noop_lineto" {
try pathTestRun(testing.allocator, _041_stroke_noop_lineto);
}

//////////////////////////////////////////////////////////////////////////////

fn compositorExportRun(alloc: mem.Allocator, subject: anytype) !void {
Expand Down
38 changes: 36 additions & 2 deletions src/internal/FillPlotter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const std = @import("std");
const debug = @import("std").debug;
const mem = @import("std").mem;
const testing = @import("std").testing;

// const options = @import("../options.zig");
const nodepkg = @import("path_nodes.zig");
Expand Down Expand Up @@ -59,8 +60,10 @@ pub fn plot(
if (current_point == null) return InternalError.InvalidState;
if (current_polygon == null) return InternalError.InvalidState;

try current_polygon.?.plot(n.point, null);
current_point = n.point;
if (!current_point.?.equal(n.point)) {
try current_polygon.?.plot(n.point, null);
current_point = n.point;
}
},
.curve_to => |n| {
if (initial_point == null) return InternalError.InvalidState;
Expand Down Expand Up @@ -114,3 +117,34 @@ const SplinePlotterCtx = struct {
self.current_point.* = node.point;
}
};

test "degenerate line_to" {
const alloc = testing.allocator;
var nodes = std.ArrayList(nodepkg.PathNode).init(alloc);
defer nodes.deinit();
try nodes.append(.{ .move_to = .{ .point = .{ .x = 5, .y = 0 } } });
try nodes.append(.{ .line_to = .{ .point = .{ .x = 10, .y = 10 } } });
try nodes.append(.{ .line_to = .{ .point = .{ .x = 10, .y = 10 } } });
try nodes.append(.{ .line_to = .{ .point = .{ .x = 0, .y = 10 } } });
try nodes.append(.{ .close_path = .{} });
try nodes.append(.{ .move_to = .{ .point = .{ .x = 5, .y = 0 } } });

var result = try plot(alloc, nodes, 1, 0.1);
defer result.deinit();
try testing.expectEqual(1, result.polygons.items.len);
var corners_len: usize = 0;
var corners = std.ArrayList(Point).init(alloc);
defer corners.deinit();
var next_: ?*Polygon.CornerList.Node = result.polygons.items[0].corners.first;
while (next_) |n| {
try corners.append(n.data);
corners_len += 1;
next_ = n.next;
}
try testing.expectEqual(3, corners_len);
try testing.expectEqualSlices(Point, &.{
.{ .x = 5, .y = 0 },
.{ .x = 10, .y = 10 },
.{ .x = 0, .y = 10 },
}, corners.items);
}
40 changes: 21 additions & 19 deletions src/internal/StrokePlotter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -522,27 +522,29 @@ const Iterator = struct {
fn line_to(self: *State, node: nodepkg.PathLineTo) !bool {
if (self.initial_point_ != null) {
if (self.current_point_) |current_point| {
if (self.last_point_) |last_point| {
// Join the lines last -> current -> node, with
// the join points representing the points
// around current.
const clockwise = try self.it.join(
&self.outer,
&self.inner,
last_point,
current_point,
node.point,
self.clockwise_,
null,
);
if (self.clockwise_ == null) self.clockwise_ = clockwise;
if (!current_point.equal(node.point)) { // No-op on degenerate line_to
if (self.last_point_) |last_point| {
// Join the lines last -> current -> node, with
// the join points representing the points
// around current.
const clockwise = try self.it.join(
&self.outer,
&self.inner,
last_point,
current_point,
node.point,
self.clockwise_,
null,
);
if (self.clockwise_ == null) self.clockwise_ = clockwise;
}
if (self.first_line_point_ == null) {
self.first_line_point_ = node.point;
}
self.last_point_ = self.current_point_;
self.current_point_ = node.point;
}
} else return InternalError.InvalidState; // move_to always sets both initial and current points
if (self.first_line_point_ == null) {
self.first_line_point_ = node.point;
}
self.last_point_ = self.current_point_;
self.current_point_ = node.point;
} else return InternalError.InvalidState; // line_to should never be called internally without move_to

return true;
Expand Down
1 change: 1 addition & 0 deletions src/z2d.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ pub const Surface = surface.Surface;

test {
@import("std").testing.refAllDecls(@This());
_ = @import("internal/FillPlotter.zig");
_ = @import("internal/StrokePlotter.zig");
}

0 comments on commit 21c4d25

Please sign in to comment.