Skip to content

Commit 774eeb2

Browse files
shai-almogclaude
andcommitted
iOS Metal port: fix roundRectPath pill triangular tear
The four-corner round-rect path was tracing each corner with sweep=+pi/2 on start angles -pi/2, 0, +pi/2, pi. Because cn1's GeneralPath.arc() internally negates the user-facing angles before feeding them to Ellipse.getPointAtAngle (which uses cy + b*sin(theta) on screen-Y-down coords), each corner's arc actually traversed the *opposite* quadrant of its bbox -- top-right's arc started at the bottom of the right ellipse instead of the top, etc. For non-pill round-rects the bug was masked: each corner bbox sits in its own region of the rectangle, so even an arc tracing the wrong quadrant produced an outline that closed up around the rect interior. The joinPath=true lineTos between corners crossed the bbox interiors (e.g. top-right corner picked up a vertical line from (top-edge end) to (corner-bottom)), but the winding-fill rasterizer happened to fill the overall area the same way. For pills (arcWidth == arcHeight == box height, so rx == ry == h/2 and adjacent corners share a bbox because h-2ry == 0) the broken arcs overlapped each other and the connecting lineTos cut a triangular gash through the pill. Visible in SwitchTheme / kotlin: the right half of the toggle track collapsed into a wedge with the thumb sitting in the gap. The earlier fix attempts (5fb3f6a joinPath=true, 15e1d9f skip zero-length edges) were addressing symptoms of the same wrong-quadrant issue. Switch to a CW screen-coord traversal where each corner's start angle matches where the previous edge ended and sweep=-pi/2 traces the corner's own quadrant: top-right : start +pi/2 (top), sweep -pi/2 -> right bottom-right : start 0 (right), sweep -pi/2 -> bottom bottom-left : start -pi/2 (bottom), sweep -pi/2 -> left top-left : start +pi (left), sweep -pi/2 -> top Apply the same correction in MutableGraphics.roundRectPath and GlobalGraphics.roundRectPath so screen and mutable rendering stay consistent. The mutable alpha-mask pipeline now renders Switch tracks as full pills end-to-end on Metal -- verified locally on iPhone 16 Pro against the kotlin test golden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1330414 commit 774eeb2

1 file changed

Lines changed: 44 additions & 25 deletions

File tree

Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4914,32 +4914,51 @@ private GeneralPath roundRectPath(int x, int y, int width, int height, int arcWi
49144914
p.closePath();
49154915
return p;
49164916
}
4917-
// joinPath=true on each arc so the corners connect to the
4918-
// adjacent line segments via lineTo instead of starting a new
4919-
// sub-path. With joinPath=false (the prior code) each arc was
4920-
// an independent moveTo'd sub-path, and Renderer.c rendered the
4921-
// whole thing as 4 disconnected pacman pieces.
4917+
// Trace the round-rect outline as a single closed sub-path going
4918+
// CW in screen coords (Y-down): top edge -> top-right corner ->
4919+
// right edge -> bottom-right corner -> bottom edge -> bottom-left
4920+
// corner -> left edge -> top-left corner -> close.
49224921
//
4923-
// Skip the lineTos when their endpoints would coincide with the
4924-
// arc's join target (pill case: rx == width/2 collapses the top
4925-
// and bottom edges; ry == height/2 collapses the left and right
4926-
// edges). Emitting a zero-length lineTo into the path leaves a
4927-
// phantom edge that Renderer.c's winding pass interprets as a
4928-
// tear, which is what made the Switch pill render with a
4929-
// triangular wedge cut into it.
4922+
// Each corner arc starts where the previous edge ended and ends
4923+
// where the next edge begins, so every joinPath=true draws a
4924+
// zero-length connector. cn1's GeneralPath.arc internally negates
4925+
// the angles (math-Y-up convention -> screen-Y-down via
4926+
// -startAngle / -sweepAngle in addToPath), and getPointAtAngle
4927+
// uses cy + b*sin(theta), which means a user-facing angle of
4928+
// +pi/2 evaluates to (cx, cy - b) in screen coords (top of bbox)
4929+
// and 0 evaluates to (cx + a, cy) (right). Sweep -pi/2 traces a
4930+
// single quadrant CW visually. Concretely:
4931+
// top-right : start +pi/2 (top), sweep -pi/2 -> right
4932+
// bottom-right : start 0 (right), sweep -pi/2 -> bottom
4933+
// bottom-left : start -pi/2 (bottom), sweep -pi/2 -> left
4934+
// top-left : start +pi (left), sweep -pi/2 -> top
4935+
// The previous version used sweep +pi/2 with the opposite start
4936+
// angles, which produced an arc traversing the *opposite*
4937+
// quadrant of the corner bbox. For pills (where adjacent corner
4938+
// bboxes overlap because h-2ry == 0) the resulting path looped
4939+
// back through the bbox interior and Renderer.c's winding-fill
4940+
// pass interpreted that as a tear -- visible as the Switch
4941+
// track's right-half collapsing into a triangular wedge.
4942+
//
4943+
// Skip the inter-corner lineTos when their endpoints would
4944+
// coincide with the next arc's join target (pill case: rx ==
4945+
// width/2 collapses the top/bottom edges; ry == height/2
4946+
// collapses the left/right edges). Emitting a zero-length lineTo
4947+
// would leave a phantom edge that the winding pass also reads
4948+
// as a tear.
49304949
boolean hasTopBottomEdges = rx < width / 2f;
49314950
boolean hasLeftRightEdges = ry < height / 2f;
49324951
float twoRx = 2f * rx;
49334952
float twoRy = 2f * ry;
49344953
p.moveTo(x + rx, y);
49354954
if (hasTopBottomEdges) p.lineTo(x + width - rx, y);
4936-
p.arc(x + width - twoRx, y, twoRx, twoRy, -Math.PI / 2, Math.PI / 2, true);
4955+
p.arc(x + width - twoRx, y, twoRx, twoRy, Math.PI / 2, -Math.PI / 2, true);
49374956
if (hasLeftRightEdges) p.lineTo(x + width, y + height - ry);
4938-
p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, Math.PI / 2, true);
4957+
p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, -Math.PI / 2, true);
49394958
if (hasTopBottomEdges) p.lineTo(x + rx, y + height);
4940-
p.arc(x, y + height - twoRy, twoRx, twoRy, Math.PI / 2, Math.PI / 2, true);
4959+
p.arc(x, y + height - twoRy, twoRx, twoRy, -Math.PI / 2, -Math.PI / 2, true);
49414960
if (hasLeftRightEdges) p.lineTo(x, y + ry);
4942-
p.arc(x, y, twoRx, twoRy, Math.PI, Math.PI / 2, true);
4961+
p.arc(x, y, twoRx, twoRy, Math.PI, -Math.PI / 2, true);
49434962
p.closePath();
49444963
return p;
49454964
}
@@ -5475,24 +5494,24 @@ private GeneralPath roundRectPath(int x, int y, int width, int height, int arcWi
54755494
p.closePath();
54765495
return p;
54775496
}
5478-
// joinPath=true on each arc so corners connect to adjacent line
5479-
// segments via lineTo (single sub-path); skip lineTos that
5480-
// would have zero length (pill / circle cases). See
5481-
// MutableGraphics.roundRectPath for the rendering bug this
5482-
// avoids.
5497+
// CW screen-coord traversal with sweep=-pi/2 per corner -- see
5498+
// MutableGraphics.roundRectPath above for the angle-convention
5499+
// analysis and why sweep=+pi/2 (the prior code) traced the
5500+
// opposite quadrant of each corner bbox and produced a
5501+
// triangular tear on pills.
54835502
boolean hasTopBottomEdges = rx < width / 2f;
54845503
boolean hasLeftRightEdges = ry < height / 2f;
54855504
float twoRx = 2f * rx;
54865505
float twoRy = 2f * ry;
54875506
p.moveTo(x + rx, y);
54885507
if (hasTopBottomEdges) p.lineTo(x + width - rx, y);
5489-
p.arc(x + width - twoRx, y, twoRx, twoRy, -Math.PI / 2, Math.PI / 2, true);
5508+
p.arc(x + width - twoRx, y, twoRx, twoRy, Math.PI / 2, -Math.PI / 2, true);
54905509
if (hasLeftRightEdges) p.lineTo(x + width, y + height - ry);
5491-
p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, Math.PI / 2, true);
5510+
p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, -Math.PI / 2, true);
54925511
if (hasTopBottomEdges) p.lineTo(x + rx, y + height);
5493-
p.arc(x, y + height - twoRy, twoRx, twoRy, Math.PI / 2, Math.PI / 2, true);
5512+
p.arc(x, y + height - twoRy, twoRx, twoRy, -Math.PI / 2, -Math.PI / 2, true);
54945513
if (hasLeftRightEdges) p.lineTo(x, y + ry);
5495-
p.arc(x, y, twoRx, twoRy, Math.PI, Math.PI / 2, true);
5514+
p.arc(x, y, twoRx, twoRy, Math.PI, -Math.PI / 2, true);
54965515
p.closePath();
54975516
return p;
54985517
}

0 commit comments

Comments
 (0)