From 149f8477c338fc9ab26a9f10779309555ed1cf37 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis <69913791+ReinisSprogis@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:04:28 +0200 Subject: [PATCH] fix: prevent crash when zooming far into `Polygon`s (#1854) Co-authored-by: Luka S --- lib/src/layer/polygon_layer/painter.dart | 179 +++++++++++++++++++---- 1 file changed, 149 insertions(+), 30 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index a0b6eef93..a26202bee 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -28,6 +28,13 @@ class _PolygonPainter extends CustomPainter { final _hits = []; // Avoids repetitive memory reallocation + // OutCodes for the Cohen-Sutherland algorithm + static const _csInside = 0; // 0000 + static const _csLeft = 1; // 0001 + static const _csRight = 2; // 0010 + static const _csBottom = 4; // 0100 + static const _csTop = 8; // 1000 + /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, @@ -212,6 +219,9 @@ class _PolygonPainter extends CustomPainter { origin: origin, points: projectedPolygon.points, ), + size, + _getBorderPaint(polygon), + canvas, ); } @@ -234,7 +244,8 @@ class _PolygonPainter extends CustomPainter { } if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { - _addHoleBordersToPath(borderPath, polygon, holeOffsetsList); + _addHoleBordersToPath(borderPath, polygon, holeOffsetsList, size, + canvas, _getBorderPaint(polygon)); } } @@ -246,7 +257,7 @@ class _PolygonPainter extends CustomPainter { // ensure polygons and labels are stacked correctly, i.e.: // p1, p1_label, p2, p2_label, ... . - // The painter will be null if the layouting algorithm determined that + // The painter will be null if the layOuting algorithm determined that // there isn't enough space. final painter = _buildLabelTextPainter( mapSize: camera.size, @@ -307,11 +318,15 @@ class _PolygonPainter extends CustomPainter { Path path, Polygon polygon, List offsets, + Size canvasSize, + Paint paint, + Canvas canvas, ) { if (polygon.isDotted) { final borderRadius = polygon.borderStrokeWidth / 2; final spacing = polygon.borderStrokeWidth * 1.5; - _addDottedLineToPath(path, offsets, borderRadius, spacing); + _addDottedLineToPath( + canvas, paint, offsets, borderRadius, spacing, canvasSize); } else { _addLineToPath(path, offsets); } @@ -321,12 +336,16 @@ class _PolygonPainter extends CustomPainter { Path path, Polygon polygon, List> holeOffsetsList, + Size canvasSize, + Canvas canvas, + Paint paint, ) { if (polygon.isDotted) { final borderRadius = polygon.borderStrokeWidth / 2; final spacing = polygon.borderStrokeWidth * 1.5; for (final offsets in holeOffsetsList) { - _addDottedLineToPath(path, offsets, borderRadius, spacing); + _addDottedLineToPath( + canvas, paint, offsets, borderRadius, spacing, canvasSize); } } else { for (final offsets in holeOffsetsList) { @@ -335,41 +354,141 @@ class _PolygonPainter extends CustomPainter { } } + // Function to clip a line segment to a rectangular area (canvas) + List? _getVisibleSegment(Offset p0, Offset p1, Size canvasSize) { + // Function to compute the outCode for a point relative to the canvas + int computeOutCode( + double x, + double y, + double xMin, + double yMin, + double xMax, + double yMax, + ) { + int code = _csInside; + + if (x < xMin) { + code |= _csLeft; + } else if (x > xMax) { + code |= _csRight; + } + if (y < yMin) { + code |= _csBottom; + } else if (y > yMax) { + code |= _csTop; + } + + return code; + } + + const double xMin = 0; + const double yMin = 0; + final double xMax = canvasSize.width; + final double yMax = canvasSize.height; + + double x0 = p0.dx; + double y0 = p0.dy; + double x1 = p1.dx; + double y1 = p1.dy; + + int outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax); + int outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax); + bool accept = false; + + while (true) { + if ((outCode0 | outCode1) == 0) { + // Both points inside; trivially accept + accept = true; + break; + } else if ((outCode0 & outCode1) != 0) { + // Both points share an outside zone; trivially reject + break; + } else { + // Could be partially inside; calculate intersection + double x; + double y; + final int outCodeOut = outCode0 != 0 ? outCode0 : outCode1; + + if ((outCodeOut & _csTop) != 0) { + x = x0 + (x1 - x0) * (yMax - y0) / (y1 - y0); + y = yMax; + } else if ((outCodeOut & _csBottom) != 0) { + x = x0 + (x1 - x0) * (yMin - y0) / (y1 - y0); + y = yMin; + } else if ((outCodeOut & _csRight) != 0) { + y = y0 + (y1 - y0) * (xMax - x0) / (x1 - x0); + x = xMax; + } else if ((outCodeOut & _csLeft) != 0) { + y = y0 + (y1 - y0) * (xMin - x0) / (x1 - x0); + x = xMin; + } else { + // This else block should never be reached. + break; + } + + // Update the point and outCode + if (outCodeOut == outCode0) { + x0 = x; + y0 = y; + outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax); + } else { + x1 = x; + y1 = y; + outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax); + } + } + } + + if (accept) { + // Make sure we return the points within the canvas + return [Offset(x0, y0), Offset(x1, y1)]; + } + return null; + } + void _addDottedLineToPath( - Path path, + Canvas canvas, + Paint paint, List offsets, double radius, double stepLength, + Size canvasSize, ) { if (offsets.isEmpty) { return; } - double startDistance = 0; - for (int i = 0; i < offsets.length; i++) { - final o0 = offsets[i % offsets.length]; - final o1 = offsets[(i + 1) % offsets.length]; - final totalDistance = (o0 - o1).distance; - - double distance = startDistance; - while (distance < totalDistance) { - 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; + // Calculate for all segments, including closing the loop from the last to the first point + final int totalOffsets = offsets.length; + for (int i = 0; i < totalOffsets; i++) { + final Offset start = offsets[i % totalOffsets]; + final Offset end = + offsets[(i + 1) % totalOffsets]; // Wrap around to the first point + + // Attempt to adjust the segment to the visible part of the canvas + final List? visibleSegment = + _getVisibleSegment(start, end, canvasSize); + if (visibleSegment == null) { + continue; // Skip if the segment is completely outside } - startDistance = distance < totalDistance - ? stepLength - (totalDistance - distance) - : distance - totalDistance; + final Offset adjustedStart = visibleSegment[0]; + final Offset adjustedEnd = visibleSegment[1]; + final double lineLength = (adjustedStart - adjustedEnd).distance; + final Offset stepVector = + (adjustedEnd - adjustedStart) / lineLength * stepLength; + double traveledDistance = 0; + + Offset currentPoint = adjustedStart; + while (traveledDistance < lineLength) { + // Draw the circle if within the canvas bounds (additional check now redundant) + canvas.drawCircle(currentPoint, radius, paint); + + // Move to the next point + currentPoint = currentPoint + stepVector; + traveledDistance += stepLength; + } } - - path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); } void _addLineToPath(Path path, List offsets) { @@ -377,10 +496,10 @@ class _PolygonPainter extends CustomPainter { } ({Offset min, Offset max}) _getBounds(Offset origin, Polygon polygon) { - final bbox = polygon.boundingBox; + final bBox = polygon.boundingBox; return ( - min: getOffset(camera, origin, bbox.southWest), - max: getOffset(camera, origin, bbox.northEast), + min: getOffset(camera, origin, bBox.southWest), + max: getOffset(camera, origin, bBox.northEast), ); }