From 01678911e2b504bff73dba79534989ebb35af804 Mon Sep 17 00:00:00 2001 From: Luka S Date: Sat, 8 Jun 2024 10:25:47 +0100 Subject: [PATCH] fix: overlapping `Polygon` cutting & color/translucency mixing (#1901) --- example/lib/pages/polygon.dart | 65 ++++++++++++++++++++++++ lib/src/layer/polygon_layer/painter.dart | 48 +++++++++++++---- lib/src/layer/polygon_layer/polygon.dart | 14 +++++ 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 978e5f54f..af1526898 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -188,6 +188,71 @@ class _PolygonPageState extends State { subtitle: 'This one still works with performant rendering', ), ), + Polygon( + points: const [ + LatLng(61.861042, 0.946502), + LatLng(61.861458, 0.949468), + LatLng(61.861427, 0.949626), + LatLng(61.859015, 0.951513), + LatLng(61.858129, 0.952652) + ], + holePointsList: [], + color: Colors.lightGreen.withOpacity(0.5), + borderColor: Colors.lightGreen.withOpacity(0.5), + borderStrokeWidth: 10, + hitValue: ( + title: 'Testing opacity treatment (small)', + subtitle: + "Holes shouldn't be cut, and colors should be mixed correctly", + ), + ), + Polygon( + points: const [ + LatLng(61.861042, 0.946502), + LatLng(61.861458, 0.949468), + LatLng(61.861427, 0.949626), + LatLng(61.859015, 0.951513), + LatLng(61.858129, 0.952652), + LatLng(61.857633, 0.953214), + LatLng(61.855842, 0.954683), + LatLng(61.855769, 0.954692), + LatLng(61.855679, 0.954565), + LatLng(61.855417, 0.953926), + LatLng(61.855268, 0.953431), + LatLng(61.855173, 0.952443), + LatLng(61.855161, 0.951147), + LatLng(61.855222, 0.950822), + LatLng(61.855928, 0.948422), + LatLng(61.856365, 0.946638), + LatLng(61.856456, 0.946586), + LatLng(61.856787, 0.946656), + LatLng(61.857578, 0.946675), + LatLng(61.859338, 0.946453), + LatLng(61.861042, 0.946502) + ], + holePointsList: const [ + [ + LatLng(61.858881, 0.947234), + LatLng(61.858728, 0.947126), + LatLng(61.858562, 0.947132), + LatLng(61.858458, 0.947192), + LatLng(61.85844, 0.947716), + LatLng(61.858488, 0.947819), + LatLng(61.858766, 0.947818), + LatLng(61.858893, 0.947779), + LatLng(61.858975, 0.947542), + LatLng(61.858881, 0.947234) + ] + ], + color: Colors.lightGreen.withOpacity(0.5), + borderColor: Colors.lightGreen.withOpacity(0.5), + borderStrokeWidth: 10, + hitValue: ( + title: 'Testing opacity treatment (large)', + subtitle: + "Holes shouldn't be cut, and colors should be mixed correctly", + ), + ), ]; late final _polygons = Map.fromEntries(_polygonsRaw.map((e) => MapEntry(e.hitValue, e))); diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 1952ee8ca..d2f36e27c 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -16,10 +16,20 @@ base class _PolygonPainter /// Reference to the bounding box of the [Polygon]. final LatLngBounds bounds; - /// Whether to draw per-polygon labels + /// Whether to draw per-polygon labels ([Polygon.label]) + /// + /// Note that drawing labels will reduce performance, as the internal + /// canvas must be drawn to and 'saved' more frequently to ensure the proper + /// stacking order is maintained. This can be avoided, potentially at the + /// expense of appearance, by setting [PolygonLayer.drawLabelsLast]. + /// + /// It is safe to ignore this property, and the performance pitfalls described + /// above, if no [Polygon]s have labels specified. final bool polygonLabels; /// Whether to draw labels last and thus over all the polygons + /// + /// This may improve performance: see [polygonLabels] for more information. final bool drawLabelsLast; /// Create a new [_PolygonPainter] instance. @@ -85,6 +95,8 @@ base class _PolygonPainter @override void paint(Canvas canvas, Size size) { + const checkOpacity = true; // for debugging purposes only, should be true + final trianglePoints = []; final filledPath = Path(); @@ -154,8 +166,12 @@ base class _PolygonPainter // 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. + // We also need to flush if the opacity is not 1 or 0, so that they get + // mixed properly. Otherwise, holes get cut, or colors aren't mixed, + // depending on the holes handler. final hash = polygon.renderHashCode; - if (lastHash != hash) { + final opacity = polygon.color?.opacity ?? 0; + if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { drawPaths(); } lastPolygon = polygon; @@ -193,13 +209,11 @@ base class _PolygonPainter } // Afterwards deal with more complicated holes. + // Improper handling of opacity and fill methods may result in normal + // polygons cutting holes into other polygons, when they should be mixing: + // https://github.com/fleaflet/flutter_map/issues/1898. 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. - filledPath.fillType = PathFillType.evenOdd; - final holeOffsetsList = List>.generate( holePointsList.length, (i) => getOffsets(camera, origin, holePointsList[i]), @@ -208,11 +222,27 @@ base class _PolygonPainter for (final holeOffsets in holeOffsetsList) { filledPath.addPolygon(holeOffsets, true); + + // TODO: Potentially more efficient and may change the need to do + // opacity checking - needs testing. However, + // https://github.com/flutter/flutter/issues/44572 prevents this. + /*filledPath = Path.combine( + PathOperation.difference, + filledPath, + Path()..addPolygon(holeOffsets, true), + );*/ } if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { - _addHoleBordersToPath(borderPath, polygon, holeOffsetsList, size, - canvas, _getBorderPaint(polygon), polygon.borderStrokeWidth); + _addHoleBordersToPath( + borderPath, + polygon, + holeOffsetsList, + size, + canvas, + _getBorderPaint(polygon), + polygon.borderStrokeWidth, + ); } } diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 2aaf08e53..14436a0ec 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -18,6 +18,11 @@ class Polygon { final List>? holePointsList; /// The fill color of the [Polygon]. + /// + /// Note that translucent (opacity is not 1 or 0) colors will reduce + /// performance, as the internal canvas must be drawn to and 'saved' more + /// frequently to ensure the colors of overlapping polygons are mixed + /// correctly. final Color? color; /// The stroke width of the [Polygon] outline. @@ -69,6 +74,11 @@ class Polygon { final StrokeJoin strokeJoin; /// The optional label of the [Polygon]. + /// + /// Note that specifying a label will reduce performance, as the internal + /// canvas must be drawn to and 'saved' more frequently to ensure the proper + /// stacking order is maintained. This can be avoided, potentially at the + /// expense of appearance, by setting [PolygonLayer.drawLabelsLast]. final String? label; /// The [TextStyle] of the [Polygon.label]. @@ -78,6 +88,8 @@ class Polygon { /// /// [PolygonLabelPlacement.polylabel] can be expensive for some polygons. If /// there is a large lag spike, try using [PolygonLabelPlacement.centroid]. + /// + /// Labels will not be drawn if there is not enough space. final PolygonLabelPlacement labelPlacement; /// Whether to rotate the label counter to the camera's rotation, to ensure @@ -187,6 +199,8 @@ class Polygon { int? _renderHashCode; /// An optimized hash code dedicated to be used inside the [_PolygonPainter]. + /// + /// Note that opacity is handled in the painter. int get renderHashCode => _renderHashCode ??= Object.hash( color, borderStrokeWidth,