strokeTriangles
Generates triangles to stroke path.
Vertexes will have unset uv and color is alpha multiplied opts.color fading to transparent at the edge.
Parameters
- path:Path
- Path
- allocator:std.mem.Allocator
- std.mem.Allocator
- opts:StrokeOptions
- StrokeOptions
Source
Implementation
pub fn strokeTriangles(path: Path, allocator: std.mem.Allocator, opts: StrokeOptions) std.mem.Allocator.Error!Triangles {
if (dvui.clipGet().empty()) {
return .empty;
}
if (path.points.len == 1) {
// draw a circle with radius thickness at that point
const center = path.points[0];
const other_allocator = if (dvui.current_window) |cw|
if (cw.lifo().ptr != allocator.ptr) cw.lifo() else cw.arena()
else
// Using the same allocator will "leak" the tempPath on
// arena allocators because it can only free the last allocation
allocator;
var tempPath: Path.Builder = .init(other_allocator);
defer tempPath.deinit();
tempPath.addArc(center, opts.thickness, math.pi * 2.0, 0, true);
return tempPath.build().fillConvexTriangles(allocator, .{ .color = opts.color, .fade = 1.0 });
}
const Side = enum {
none,
left,
right,
};
// a single segment can't be closed
const closed: bool = if (path.points.len == 2) false else opts.closed;
var vtx_count = path.points.len * 8;
if (!closed) {
vtx_count += 4;
}
// max is 18 per leg (2 tri fill plus 4 tri fade)
// plus (if miter is too long) 18 for fill and fade at each corner
var idx_count = (path.points.len - 1) * 18 + (path.points.len - 2) * 18;
if (closed) {
idx_count += 18 + 2 * 18;
} else {
idx_count += 8 * 3;
}
var builder = try Triangles.Builder.init(allocator, vtx_count, idx_count);
const col: Color.PMA = .fromColor(opts.color);
const aa_size = 1.0;
var vtx_left: u16 = 0;
var vtx_right: u16 = 0;
var i: usize = 0;
const last_i: usize = if (closed) path.points.len + 1 else path.points.len;
while (i < last_i) : (i += 1) {
const ai: u16 = @intCast((i + path.points.len - 1) % path.points.len);
const bi: u16 = @intCast(i % path.points.len);
const ci: u16 = @intCast((i + 1) % path.points.len);
const aa = path.points[ai];
var bb = path.points[bi];
const cc = path.points[ci];
var miter_break: Side = .none;
// the amount to move from bb to the edge of the line
var halfnorm: Point.Physical = undefined;
var halfnorm_miter: Point.Physical = undefined;
var diffab: Point.Physical = undefined;
if (!closed and ((i == 0) or ((i + 1) == path.points.len))) {
if (i == 0) {
const diffbc = bb.diff(cc).normalize();
// rotate by 90 to get normal
halfnorm = .{ .x = diffbc.y / 2, .y = (-diffbc.x) / 2 };
if (opts.endcap_style == .square) {
// square endcaps move bb out by thickness
bb.x += diffbc.x * opts.thickness;
bb.y += diffbc.y * opts.thickness;
}
// add 2 extra vertexes for endcap fringe
builder.appendVertex(.{
.pos = .{
.x = bb.x - halfnorm.x * (opts.thickness + aa_size) + diffbc.x * aa_size,
.y = bb.y - halfnorm.y * (opts.thickness + aa_size) + diffbc.y * aa_size,
},
.col = .transparent,
});
builder.appendVertex(.{
.pos = .{
.x = bb.x + halfnorm.x * (opts.thickness + aa_size) + diffbc.x * aa_size,
.y = bb.y + halfnorm.y * (opts.thickness + aa_size) + diffbc.y * aa_size,
},
.col = .transparent,
});
// add indexes for endcap fringe
builder.appendTriangles(&.{
0, 2, 3,
0, 1, 2,
1, 4, 2,
1, 5, 4,
});
} else if ((i + 1) == path.points.len) {
diffab = aa.diff(bb).normalize();
// rotate by 90 to get normal
halfnorm = .{ .x = diffab.y / 2, .y = (-diffab.x) / 2 };
if (opts.endcap_style == .square) {
// square endcaps move bb out by thickness
bb.x -= diffab.x * opts.thickness;
bb.y -= diffab.y * opts.thickness;
}
}
} else {
diffab = aa.diff(bb).normalize();
const diffbc = bb.diff(cc).normalize();
// average of normals on each side
halfnorm = .{ .x = (diffab.y + diffbc.y) / 2, .y = (-diffab.x - diffbc.x) / 2 };
// scale averaged normal by angle between which happens to be the same as
// dividing by the length^2
const d2 = halfnorm.x * halfnorm.x + halfnorm.y * halfnorm.y;
if (d2 > 0.000001) {
halfnorm = halfnorm.scale(0.5 / d2, Point.Physical);
} else {
// degenerate case - ab and bc are on top of each other
halfnorm = aa.diff(bb);
}
// limit distance our vertexes can be from the point to 2 * thickness so
// very small angles don't produce huge geometries
const l = halfnorm.length();
if (l > 2.0) {
halfnorm_miter = halfnorm.scale(2.0 / l, Point.Physical);
const hn_len = halfnorm.length() * (opts.thickness + aa_size);
const ab_len = aa.diff(bb).length();
const bc_len = bb.diff(cc).length();
const max_len = @min(hn_len, ab_len, bc_len);
if (max_len < hn_len) {
halfnorm = halfnorm.scale(max_len / hn_len, Point.Physical);
}
const dot = diffab.x * halfnorm.x + diffab.y * halfnorm.y;
if (dot > 0) {
miter_break = .left;
} else {
miter_break = .right;
}
}
}
const vtx_base: u16 = if (i == path.points.len) 0 else @intCast(builder.vertexes.items.len);
var vtx_left_in = vtx_base;
var vtx_right_in = vtx_left_in + 2;
var vtx_left_out = vtx_left_in;
var vtx_right_out = vtx_right_in;
switch (miter_break) {
.none => {},
.left => {
vtx_left_in += 4;
vtx_left_out += 6;
},
.right => {
vtx_right_in += 2;
vtx_right_out += 4;
},
}
if (i < path.points.len) {
{
const hn = if (miter_break == .left) halfnorm_miter else halfnorm;
// left inner vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * opts.thickness,
.y = bb.y - hn.y * opts.thickness,
},
.col = col,
});
// left AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * (opts.thickness + aa_size),
.y = bb.y - hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
}
{
const hn = if (miter_break == .right) halfnorm_miter else halfnorm;
// right inner vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * opts.thickness,
.y = bb.y + hn.y * opts.thickness,
},
.col = col,
});
// right AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * (opts.thickness + aa_size),
.y = bb.y + hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
}
switch (miter_break) {
.none => {},
.left => {
const hn = .{ .x = diffab.y / 2, .y = (-diffab.x) / 2 };
// left bump start inner (vtx_left_in)
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * opts.thickness,
.y = bb.y - hn.y * opts.thickness,
},
.col = col,
});
// left bump start AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * (opts.thickness + aa_size),
.y = bb.y - hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
// right bump start inner (vtx_left_out)
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * opts.thickness,
.y = bb.y + hn.y * opts.thickness,
},
.col = col,
});
// right bump start AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * (opts.thickness + aa_size),
.y = bb.y + hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
// add triangles to fill the miter (counter clockwise y going down)
builder.appendTriangles(&.{
// indexes for fill
vtx_left_in, vtx_base + 2, vtx_base,
vtx_base, vtx_base + 2, vtx_left_out,
// aa fade
vtx_left_in, vtx_base, vtx_left_in + 1,
vtx_base, vtx_base + 1, vtx_left_in + 1,
vtx_base, vtx_left_out, vtx_left_out + 1,
vtx_base, vtx_left_out + 1, vtx_base + 1,
});
},
.right => {
const hn = .{ .x = diffab.y / 2, .y = (-diffab.x) / 2 };
// right bump start inner (vtx_right_in)
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * opts.thickness,
.y = bb.y + hn.y * opts.thickness,
},
.col = col,
});
// right bump start AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x + hn.x * (opts.thickness + aa_size),
.y = bb.y + hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
// left bump start inner (vtx_right_out)
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * opts.thickness,
.y = bb.y - hn.y * opts.thickness,
},
.col = col,
});
// left bump start AA vertex
builder.appendVertex(.{
.pos = .{
.x = bb.x - hn.x * (opts.thickness + aa_size),
.y = bb.y - hn.y * (opts.thickness + aa_size),
},
.col = .transparent,
});
// add triangles to fill the miter (counter clockwise y going down)
builder.appendTriangles(&.{
// indexes for fill
vtx_right_in, vtx_base + 2, vtx_base,
vtx_base, vtx_base + 2, vtx_right_out,
// aa fade
vtx_right_in, vtx_right_in + 1, vtx_base + 2,
vtx_right_in + 1, vtx_base + 3, vtx_base + 2,
vtx_base + 2, vtx_base + 3, vtx_right_out,
vtx_base + 3, vtx_right_out + 1, vtx_right_out,
});
},
}
}
// triangles must be counter-clockwise (y going down) to avoid backface culling
if ((i > 0) and (closed or (i < path.points.len))) {
// vtx1 wraps when closed
builder.appendTriangles(&.{
// indexes for fill
vtx_left, vtx_right, vtx_left_in,
vtx_right, vtx_right_in, vtx_left_in,
// indexes for aa fade from inner to outer left side
vtx_left, vtx_left_in + 1, vtx_left + 1,
vtx_left, vtx_left_in, vtx_left_in + 1,
// indexes for aa fade from inner to outer right side
vtx_right, vtx_right + 1, vtx_right_in + 1,
vtx_right, vtx_right_in + 1, vtx_right_in,
});
}
if (!closed and (i + 1) == path.points.len) {
// add 2 extra vertexes for endcap fringe
const vtx: u16 = @intCast(builder.vertexes.items.len);
builder.appendVertex(.{
.pos = .{
.x = bb.x - halfnorm.x * (opts.thickness + aa_size) - diffab.x * aa_size,
.y = bb.y - halfnorm.y * (opts.thickness + aa_size) - diffab.y * aa_size,
},
.col = .transparent,
});
builder.appendVertex(.{
.pos = .{
.x = bb.x + halfnorm.x * (opts.thickness + aa_size) - diffab.x * aa_size,
.y = bb.y + halfnorm.y * (opts.thickness + aa_size) - diffab.y * aa_size,
},
.col = .transparent,
});
builder.appendTriangles(&.{
// add indexes for endcap fringe
vtx_left_in, vtx, vtx_left_in + 1,
vtx, vtx_left_in, vtx_right_in,
vtx, vtx_right_in, vtx + 1,
vtx_right_in, vtx_right_in + 1, vtx + 1,
});
}
vtx_left = vtx_left_out;
vtx_right = vtx_right_out;
}
return builder.build();
}