@@ -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+
249282void 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
0 commit comments