Skip to content

Commit 4a7a190

Browse files
committed
#17 improve vgerStrokeBezier for high curvature
1 parent b1b17f6 commit 4a7a190

3 files changed

Lines changed: 103 additions & 17 deletions

File tree

Sources/vger/vger.mm

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -246,34 +246,119 @@ void vgerStrokeRect(vgerContext vg, vector_float2 min, vector_float2 max, float
246246

247247
}
248248

249+
// Helper function to subdivide a bezier curve and collect segments
250+
static void subdivideBezierForStroke(vgerBezierSegment s, float width, std::vector<vgerBezierSegment>& segments) {
251+
const float min_length = 0.001f;
252+
253+
// Check if this curve needs subdivision
254+
float2 chord = s.c - s.a;
255+
float chord_length = simd_length(chord);
256+
257+
if (chord_length < min_length) {
258+
return; // Skip degenerate curves
259+
}
260+
261+
// Calculate curve deviation from its chord
262+
float2 mid_point = 0.25f * s.a + 0.5f * s.b + 0.25f * s.c; // Approximate curve midpoint
263+
float2 chord_mid = 0.5f * (s.a + s.c);
264+
float deviation = simd_length(mid_point - chord_mid);
265+
266+
// Subdivide if deviation is too high relative to stroke width
267+
if (deviation > width * 1.5f && chord_length > width * 2.0f) {
268+
// Subdivide using De Casteljau's algorithm
269+
float2 ab = 0.5f * (s.a + s.b);
270+
float2 bc = 0.5f * (s.b + s.c);
271+
float2 mid = 0.5f * (ab + bc);
272+
273+
// Recursively subdivide both halves
274+
subdivideBezierForStroke({s.a, ab, mid}, width, segments);
275+
subdivideBezierForStroke({mid, bc, s.c}, width, segments);
276+
} else {
277+
// Curve is acceptable, add to segments
278+
segments.push_back(s);
279+
}
280+
}
281+
249282
void vgerStrokeBezier(vgerContext vg, vgerBezierSegment s, float width, vgerPaintIndex paint) {
250283

251284
if(!vg->checkPaint(paint)) return;
252285

253286
#if 1
254-
// Improve quality of beziers by rendering as fills.
255-
256-
// 90 degrees CCW
287+
// Improved bezier stroke rendering: subdivide first, then create single path
288+
289+
// 90 degrees CCW rotation matrix
257290
float2x2 rot90{
258291
float2{0, 1},
259292
float2{-1, 0}
260293
};
294+
295+
// First, subdivide the curve if needed
296+
std::vector<vgerBezierSegment> segments;
297+
subdivideBezierForStroke(s, width, segments);
261298

262-
// Tangents.
263-
float2 d0 = rot90 * width * normalize(s.b - s.a);
264-
float2 d1 = rot90 * width * normalize(s.c - s.a);
265-
float2 d2 = rot90 * width * normalize(s.c - s.b);
266-
267-
// Don't render degenerate curves.
268-
if (any(isnan(d0)) || any(isnan(d1)) || any(isnan(d2))) {
299+
// If no valid segments, don't render
300+
if (segments.empty()) {
269301
return;
270302
}
271-
272-
vgerMoveTo(vg, s.a - d0);
273-
vgerQuadTo(vg, s.b - d1, s.c - d2);
274-
vgerLineTo(vg, s.c + d2);
275-
vgerQuadTo(vg, s.b + d1, s.a + d0);
276-
vgerLineTo(vg, s.a - d0);
303+
304+
// Generate a single closed path from all segments
305+
std::vector<float2> top_points, bottom_points;
306+
307+
for (const auto& seg : segments) {
308+
// Calculate tangent vectors for this segment
309+
float2 d0 = rot90 * width * normalize(seg.b - seg.a);
310+
float2 d1 = rot90 * width * normalize(seg.c - seg.a);
311+
float2 d2 = rot90 * width * normalize(seg.c - seg.b);
312+
313+
// Handle degenerate cases
314+
if (any(isnan(d0)) || any(isnan(d1)) || any(isnan(d2))) {
315+
float2 chord = seg.c - seg.a;
316+
float chord_length = simd_length(chord);
317+
if (chord_length < 0.001f) continue;
318+
319+
float2 chord_normal = rot90 * normalize(chord) * width;
320+
d0 = chord_normal;
321+
d1 = chord_normal;
322+
d2 = chord_normal;
323+
}
324+
325+
// Add points for top edge of stroke (left side as we trace the curve)
326+
if (top_points.empty()) {
327+
top_points.push_back(seg.a - d0); // Start point
328+
}
329+
top_points.push_back(seg.b - d1); // Control point
330+
top_points.push_back(seg.c - d2); // End point
331+
332+
// Add points for bottom edge of stroke (right side, will be traced in reverse)
333+
if (bottom_points.empty()) {
334+
bottom_points.push_back(seg.a + d0); // Start point
335+
}
336+
bottom_points.push_back(seg.b + d1); // Control point
337+
bottom_points.push_back(seg.c + d2); // End point
338+
}
339+
340+
// Create single closed path by tracing top edge forward, then bottom edge backward
341+
vgerMoveTo(vg, top_points[0]);
342+
343+
// Trace top edge with quadratic bezier segments
344+
for (size_t i = 1; i < top_points.size(); i += 2) {
345+
if (i + 1 < top_points.size()) {
346+
vgerQuadTo(vg, top_points[i], top_points[i + 1]);
347+
}
348+
}
349+
350+
// Connect to bottom edge
351+
vgerLineTo(vg, bottom_points.back());
352+
353+
// Trace bottom edge in reverse with quadratic bezier segments
354+
for (int i = (int)bottom_points.size() - 2; i >= 1; i -= 2) {
355+
if (i - 1 >= 0) {
356+
vgerQuadTo(vg, bottom_points[i], bottom_points[i - 1]);
357+
}
358+
}
359+
360+
// Close the path
361+
vgerLineTo(vg, top_points[0]);
277362
vgerFill(vg, paint);
278363

279364
#else
518 Bytes
Loading

Tests/vgerTests/vgerTests.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,11 @@ - (void) testBasic {
133133
auto white = vgerColorPaint(vg, float4{1,1,1,1});
134134
auto cyan = vgerColorPaint(vg, float4{0,1,1,1});
135135
auto magenta = vgerColorPaint(vg, float4{1,0,1,1});
136+
auto red = vgerColorPaint(vg, float4{1,0,0,1});
136137

137138
vgerFillCircle(vg, float2{256, 256}, 40, cyan);
138139
vgerStrokeBezier(vg, {{256,256}, {256,384}, {384,384}}, 1, white);
139-
vgerStrokeBezier(vg, {{384,384}, {512,512}, {512,512}}, 1, white); // Degenerate
140+
vgerStrokeBezier(vg, {{384,384}, {512,512}, {512,512}}, 1, red); // Degenerate
140141
vgerFillRect(vg, float2{400,100}, float2{450,150}, 10, vgerLinearGradient(vg, float2{400,100}, float2{450, 150}, float4{0,1,1,1}, float4{1,0,1,1}, 0));
141142
vgerFillRect(vg, float2{400,200}, float2{450,250}, 10, vgerRadialGradient(vg, float2{425,225}, 10, 40, float4{0,1,1,1}, float4{1,0,1,1}, 0));
142143
vgerStrokeArc(vg, float2{100,400}, 30, 3, theta, ap, white);

0 commit comments

Comments
 (0)