MergedSelection.java
package swingtree.style;
import org.jspecify.annotations.Nullable;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
final class MergedSelection
{
private static final double ARC_CONTROL = 0.5522847498;
private static final double SNAP_DISTANCE = 0.5;
// A simple global cache, because typically there is always only a single selection:
private static volatile @Nullable MergedSelection _lastPaintedCache;
private final List<Rectangle2D> _areas;
private final float _convexRadius;
private final float _concaveRadius;
private final float _borderWidth;
private final Shape _outline;
private final @Nullable Shape _insideTheBorder;
private MergedSelection( List<Rectangle2D> areas, float convexRadius, float concaveRadius, float borderWidth ) {
final List<List<Point2D>> outline = _outlineOf(areas);
_areas = areas;
_convexRadius = convexRadius;
_concaveRadius = concaveRadius;
_borderWidth = borderWidth;
_outline = _rounded(outline, convexRadius, concaveRadius);
_insideTheBorder = borderWidth > 0 ? _rounded(_eroded(outline, borderWidth), Math.max(0, convexRadius - borderWidth), concaveRadius + borderWidth) : null;
}
static MergedSelection of( List<Rectangle2D> areas, float convexRadius, float concaveRadius, float borderWidth ) {
final @Nullable MergedSelection last = _lastPaintedCache;
if ( last != null && last._convexRadius == convexRadius && last._concaveRadius == concaveRadius && last._borderWidth == borderWidth && last._areas.equals(areas) )
return last;
final MergedSelection merged = new MergedSelection(areas, convexRadius, concaveRadius, borderWidth);
_lastPaintedCache = merged;
return merged;
}
Shape outline() { return _outline; }
@Nullable Shape insideTheBorder() { return _insideTheBorder; }
private static List<List<Point2D>> _outlineOf( List<Rectangle2D> areas ) {
final Area union = new Area();
for ( Rectangle2D area : areas ) {
double bottom = area.getMaxY();
for ( Rectangle2D below : areas ) {
final double gap = below.getMinY() - bottom;
final boolean overlapsHorizontally = below.getMinX() < area.getMaxX() && area.getMinX() < below.getMaxX();
if ( gap > 0 && gap < SNAP_DISTANCE && overlapsHorizontally )
bottom = below.getMinY();
}
union.add(new Area(new Rectangle2D.Double(area.getX(), area.getY(), area.getWidth(), bottom - area.getY())));
}
return _polygonsOf(union);
}
private static List<List<Point2D>> _eroded( List<List<Point2D>> outline, float width ) {
if ( outline.size() == 1 ) {
final List<Point2D> polygon = outline.get(0);
final double interiorSide = Math.signum(_signedArea(polygon));
final int count = polygon.size();
final List<Point2D> inset = new ArrayList<>(count);
for ( int i = 0; i < count; i++ ) {
final Point2D previous = polygon.get((i + count - 1) % count);
final Point2D corner = polygon.get(i);
final Point2D next = polygon.get((i + 1) % count);
final double inX = _inwardX(previous, corner, interiorSide) + _inwardX(corner, next, interiorSide);
final double inY = _inwardY(previous, corner, interiorSide) + _inwardY(corner, next, interiorSide);
inset.add(new Point2D.Double(corner.getX() + width * inX, corner.getY() + width * inY));
}
if ( _keepsEveryEdgeDirection(polygon, inset) )
return Collections.singletonList(inset);
}
final Area eroded = _areaOf(outline);
for ( List<Point2D> polygon : outline ) {
for ( int i = 0; i < polygon.size(); i++ ) {
final Point2D from = polygon.get(i);
final Point2D to = polygon.get((i + 1) % polygon.size());
eroded.subtract(new Area(new Rectangle2D.Double(
Math.min(from.getX(), to.getX()) - width, Math.min(from.getY(), to.getY()) - width,
Math.abs(to.getX() - from.getX()) + 2 * width, Math.abs(to.getY() - from.getY()) + 2 * width
)));
}
}
return _polygonsOf(eroded);
}
private static Shape _rounded( List<List<Point2D>> polygons, float convexRadius, float concaveRadius ) {
final double interiorSide = Math.signum(_signedArea(_largest(polygons)));
final Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO);
for ( List<Point2D> polygon : polygons ) {
final int count = polygon.size();
for ( int i = 0; i < count; i++ ) {
final Point2D previous = polygon.get((i + count - 1) % count);
final Point2D corner = polygon.get(i);
final Point2D next = polygon.get((i + 1) % count);
final boolean isConvex = Math.signum(_turn(previous, corner, next)) == interiorSide;
final double radius = Math.min(isConvex ? convexRadius : concaveRadius, Math.min(corner.distance(previous), corner.distance(next)) / 2);
final Point2D start = _towards(corner, previous, radius);
final Point2D end = _towards(corner, next, radius);
if ( i == 0 )
path.moveTo(start.getX(), start.getY());
else
path.lineTo(start.getX(), start.getY());
if ( radius > 0 ) {
final Point2D control1 = _towards(start, corner, start.distance(corner) * ARC_CONTROL);
final Point2D control2 = _towards(end, corner, end.distance(corner) * ARC_CONTROL);
path.curveTo(control1.getX(), control1.getY(), control2.getX(), control2.getY(), end.getX(), end.getY());
}
}
path.closePath();
}
return path;
}
private static List<List<Point2D>> _polygonsOf( Area area ) {
final List<List<Point2D>> polygons = new ArrayList<>();
List<Point2D> polygon = new ArrayList<>();
final double[] coordinates = new double[6];
for ( PathIterator it = area.getPathIterator(null); !it.isDone(); it.next() ) {
final int segment = it.currentSegment(coordinates);
if ( segment == PathIterator.SEG_CLOSE ) {
final List<Point2D> corners = _withoutStraightPoints(polygon);
if ( corners.size() >= 3 )
polygons.add(corners);
polygon = new ArrayList<>();
}
else
polygon.add(new Point2D.Double(coordinates[0], coordinates[1]));
}
return polygons;
}
private static List<Point2D> _withoutStraightPoints( List<Point2D> points ) {
final List<Point2D> corners = new ArrayList<>(points);
boolean removed = true;
while ( removed && corners.size() >= 3 ) {
removed = false;
for ( int i = 0; i < corners.size(); i++ ) {
final int count = corners.size();
final Point2D previous = corners.get((i + count - 1) % count);
final Point2D next = corners.get((i + 1) % count);
if ( corners.get(i).distance(next) < 1e-6 || Math.abs(_turn(previous, corners.get(i), next)) < 1e-9 ) {
corners.remove(i);
removed = true;
break;
}
}
}
return corners;
}
private static double _inwardX( Point2D from, Point2D to, double interiorSide ) {
return -interiorSide * (to.getY() - from.getY()) / from.distance(to);
}
private static double _inwardY( Point2D from, Point2D to, double interiorSide ) {
return interiorSide * (to.getX() - from.getX()) / from.distance(to);
}
private static boolean _keepsEveryEdgeDirection( List<Point2D> polygon, List<Point2D> inset ) {
for ( int i = 0; i < polygon.size(); i++ ) {
final int j = (i + 1) % polygon.size();
final double dot = (polygon.get(j).getX() - polygon.get(i).getX()) * (inset.get(j).getX() - inset.get(i).getX())
+ (polygon.get(j).getY() - polygon.get(i).getY()) * (inset.get(j).getY() - inset.get(i).getY());
if ( dot <= 0 )
return false;
}
return true;
}
private static Area _areaOf( List<List<Point2D>> polygons ) {
final Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO);
for ( List<Point2D> polygon : polygons ) {
path.moveTo(polygon.get(0).getX(), polygon.get(0).getY());
for ( int i = 1; i < polygon.size(); i++ )
path.lineTo(polygon.get(i).getX(), polygon.get(i).getY());
path.closePath();
}
return new Area(path);
}
private static List<Point2D> _largest( List<List<Point2D>> polygons ) {
List<Point2D> largest = new ArrayList<>();
for ( List<Point2D> polygon : polygons )
if ( Math.abs(_signedArea(polygon)) > Math.abs(_signedArea(largest)) )
largest = polygon;
return largest;
}
private static double _signedArea( List<Point2D> polygon ) {
double area = 0;
for ( int i = 0; i < polygon.size(); i++ ) {
final Point2D a = polygon.get(i);
final Point2D b = polygon.get((i + 1) % polygon.size());
area += a.getX() * b.getY() - b.getX() * a.getY();
}
return area;
}
private static double _turn( Point2D previous, Point2D corner, Point2D next ) {
return (corner.getX() - previous.getX()) * (next.getY() - corner.getY()) - (corner.getY() - previous.getY()) * (next.getX() - corner.getX());
}
private static Point2D _towards( Point2D from, Point2D to, double distance ) {
final double length = from.distance(to);
if ( length == 0 )
return from;
return new Point2D.Double(from.getX() + (to.getX() - from.getX()) * distance / length, from.getY() + (to.getY() - from.getY()) * distance / length);
}
}