From 61bc7757c34a106f6c1e85062a58a8895cf86c32 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 26 Jul 2023 12:32:52 +0200 Subject: [PATCH] Fix `Polygon` edge cases (#1598) I also feel that the code re-organization really helped the separation of concerns and made it much clearer what each part is doing. Specifically: * Fill and border are clearly separated. * The loop is only responsible for batching paths. The Paint is now handled once during drawing. --- example/lib/pages/polygon.dart | 22 +++- lib/src/layer/polygon_layer.dart | 173 ++++++++++++++++++------------- 2 files changed, 122 insertions(+), 73 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 7667e39e1..8397d715f 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -98,7 +98,7 @@ class PolygonPage extends StatelessWidget { points: filledPoints, isFilled: true, color: Colors.purple, - borderColor: Colors.purple, + borderColor: Colors.yellow, borderStrokeWidth: 4, ), Polygon( @@ -120,6 +120,8 @@ class PolygonPage extends StatelessWidget { Polygon( points: labelPoints, borderStrokeWidth: 4, + isFilled: false, + color: Colors.pink, borderColor: Colors.purple, label: "Label!", ), @@ -132,12 +134,28 @@ class PolygonPage extends StatelessWidget { ), Polygon( points: holeOuterPoints, - //holePointsList: [], + isFilled: true, holePointsList: [holeInnerPoints], borderStrokeWidth: 4, borderColor: Colors.green, color: Colors.pink.withOpacity(0.5), ), + Polygon( + points: holeOuterPoints + .map((latlng) => + LatLng(latlng.latitude, latlng.longitude + 8)) + .toList(), + isFilled: false, + isDotted: true, + holePointsList: [ + holeInnerPoints + .map((latlng) => + LatLng(latlng.latitude, latlng.longitude + 8)) + .toList() + ], + borderStrokeWidth: 4, + borderColor: Colors.orange, + ), ]), ], ), diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 9044f732e..fc3c0e285 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -110,84 +110,101 @@ class PolygonPainter extends CustomPainter { int? _hash; List getOffsets(List points) { - return List.generate(points.length, (index) { - return map.getOffsetFromOrigin(points[index]); - }, growable: false); + return List.generate( + points.length, + (index) { + return map.getOffsetFromOrigin(points[index]); + }, + growable: false, + ); } @override void paint(Canvas canvas, Size size) { - var path = ui.Path(); - var paint = Paint(); + var filledPath = ui.Path(); var borderPath = ui.Path(); - Paint? borderPaint; + Polygon? lastPolygon; int? lastHash; + // This functions flushes the batched fill and border paths constructed below. void drawPaths() { - canvas.drawPath(path, paint); - path = ui.Path(); - paint = Paint(); - - if (borderPaint != null) { - canvas.drawPath(borderPath, borderPaint!); - borderPath = ui.Path(); - borderPaint = null; + if (lastPolygon == null) { + return; + } + final polygon = lastPolygon!; + + // Draw filled polygon . + if (polygon.isFilled) { + final paint = Paint() + ..style = PaintingStyle.fill + ..color = polygon.color; + + canvas.drawPath(filledPath, paint); } + + // Draw polygon outline. + if (polygon.borderStrokeWidth > 0) { + final borderPaint = _getBorderPaint(polygon); + canvas.drawPath(borderPath, borderPaint); + } + + filledPath = ui.Path(); + borderPath = ui.Path(); + lastPolygon = null; + lastHash = null; } + // Main loop constructing batched fill and border paths from given polygons. for (final polygon in polygons) { final offsets = getOffsets(polygon.points); if (offsets.isEmpty) { continue; } + // The hash is based on the polygons visual properties. If the hash from + // the current and the previous polygon no longer match, we need to flush + // the batch previous polygons. final hash = polygon.renderHashCode; - if (lastHash != null && lastHash != hash) { + if (lastHash != hash) { drawPaths(); } + lastPolygon = polygon; lastHash = hash; - final holeOffsetsList = List>.generate( - polygon.holePointsList?.length ?? 0, - (i) => getOffsets(polygon.holePointsList![i]), - growable: false); - - if (holeOffsetsList.isEmpty) { - if (polygon.isFilled) { - paint = Paint() - ..style = PaintingStyle.fill - ..strokeWidth = polygon.borderStrokeWidth - ..strokeCap = polygon.strokeCap - ..strokeJoin = polygon.strokeJoin - ..color = polygon.isFilled ? polygon.color : polygon.borderColor; - - path.addPolygon(offsets, true); - } - } else { - paint = Paint() - ..style = PaintingStyle.fill - ..color = polygon.color; + // First add fills and borders to path. + if (polygon.isFilled) { + filledPath.addPolygon(offsets, true); + } + if (polygon.borderStrokeWidth > 0.0) { + _addBorderToPath(borderPath, polygon, offsets); + } + // Afterwards deal with more complicated holes. + final holePointsList = polygon.holePointsList; + if (holePointsList != null && holePointsList.isNotEmpty) { // Ideally we'd use `Path.combine(PathOperation.difference, ...)` // instead of evenOdd fill-type, however it creates visual artifacts // using the web renderer. - path.fillType = PathFillType.evenOdd; + filledPath.fillType = PathFillType.evenOdd; + + final holeOffsetsList = List>.generate( + holePointsList.length, + (i) => getOffsets(holePointsList[i]), + growable: false, + ); - path.addPolygon(offsets, true); for (final holeOffsets in holeOffsetsList) { - path.addPolygon(holeOffsets, true); + filledPath.addPolygon(holeOffsets, true); } - } - // Only draw the border explicitly if it isn't alrady a stroke-style - // polygon. - if (polygon.borderStrokeWidth > 0.0) { - borderPaint = _getBorderPaint(polygon); - _paintBorder(borderPath, polygon, offsets, holeOffsetsList); + if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { + _addHoleBordersToPath(borderPath, polygon, holeOffsetsList); + } } if (polygon.label != null) { - // Labels are expensive they mess with draw batching. + // Labels are expensive. The `paintText` below is a canvas draw + // operation and thus requires us to reset the draw batching here. drawPaths(); Label.paintText( @@ -215,56 +232,70 @@ class PolygonPainter extends CustomPainter { ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke; } - void _paintBorder(ui.Path path, Polygon polygon, List offsets, - List> holeOffsetsList) { + void _addBorderToPath( + ui.Path path, + Polygon polygon, + List offsets, + ) { if (polygon.isDotted) { final borderRadius = (polygon.borderStrokeWidth / 2); final spacing = polygon.borderStrokeWidth * 1.5; + _addDottedLineToPath(path, offsets, borderRadius, spacing); + } else { + _addLineToPath(path, offsets); + } + } - _paintDottedLine(path, offsets, borderRadius, spacing); - - if (!polygon.disableHolesBorder) { - for (final offsets in holeOffsetsList) { - _paintDottedLine(path, offsets, borderRadius, spacing); - } + void _addHoleBordersToPath( + ui.Path path, + Polygon polygon, + List> holeOffsetsList, + ) { + if (polygon.isDotted) { + final borderRadius = (polygon.borderStrokeWidth / 2); + final spacing = polygon.borderStrokeWidth * 1.5; + for (final offsets in holeOffsetsList) { + _addDottedLineToPath(path, offsets, borderRadius, spacing); } } else { - _paintLine(path, offsets); - - if (!polygon.disableHolesBorder) { - for (final offsets in holeOffsetsList) { - _paintLine(path, offsets); - } + for (final offsets in holeOffsetsList) { + _addLineToPath(path, offsets); } } } - void _paintDottedLine( + void _addDottedLineToPath( ui.Path path, List offsets, double radius, double stepLength) { - var startDistance = 0.0; + if (offsets.isEmpty) { + return; + } + + double startDistance = 0; for (var i = 0; i < offsets.length; i++) { final o0 = offsets[i % offsets.length]; final o1 = offsets[(i + 1) % offsets.length]; final totalDistance = (o0 - o1).distance; - var distance = startDistance; - while (distance < totalDistance) { - final f1 = distance / totalDistance; - final f0 = 1.0 - f1; - final offset = Offset(o0.dx * f0 + o1.dx * f1, o0.dy * f0 + o1.dy * f1); + + double distance = startDistance; + for (; distance < totalDistance; distance += stepLength) { + final done = distance / totalDistance; + final remain = 1.0 - done; + final offset = Offset( + o0.dx * remain + o1.dx * done, + o0.dy * remain + o1.dy * done, + ); path.addOval(Rect.fromCircle(center: offset, radius: radius)); - distance += stepLength; } + startDistance = distance < totalDistance ? stepLength - (totalDistance - distance) : distance - totalDistance; } + path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); } - void _paintLine(ui.Path path, List offsets) { - if (offsets.isEmpty) { - return; - } + void _addLineToPath(ui.Path path, List offsets) { path.addPolygon(offsets, true); }