diff --git a/common/src/main/java/net/rptools/lib/GeometryUtil.java b/common/src/main/java/net/rptools/lib/GeometryUtil.java index a77e46e61e..4a2010cf36 100644 --- a/common/src/main/java/net/rptools/lib/GeometryUtil.java +++ b/common/src/main/java/net/rptools/lib/GeometryUtil.java @@ -16,29 +16,17 @@ import java.awt.Shape; import java.awt.geom.Area; +import java.awt.geom.Line2D; +import java.awt.geom.PathIterator; import java.awt.geom.Point2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; +import java.util.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.locationtech.jts.algorithm.InteriorPointArea; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.PointLocation; import org.locationtech.jts.awt.ShapeReader; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.CoordinateArrays; -import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LinearRing; -import org.locationtech.jts.geom.Location; -import org.locationtech.jts.geom.MultiPolygon; -import org.locationtech.jts.geom.Polygon; -import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.geom.*; import org.locationtech.jts.geom.util.GeometryFixer; import org.locationtech.jts.operation.valid.IsValidOp; import org.locationtech.jts.precision.GeometryPrecisionReducer; @@ -91,8 +79,8 @@ public static Area union(Collection areas) { } /** - * Like {@link #union(java.util.Collection)}, but will modify the areas and collection for - * performance gains. + * Like {@link #union(Collection)}, but will modify the areas and collection for performance + * gains. * * @param areas The areas to union. * @return The union of {@code areas} @@ -137,6 +125,62 @@ public static MultiPolygon toJts(Shape shape) { return geometry; } + /** + * Use for a simple shape that is a closed polygon without holes. + * + * @param shape a closed polygon + * @return LinearRing geometry + */ + public static LinearRing shapeToLinearRing(Shape shape) { + List coordinates = new ArrayList<>(); + final PathIterator iterator = shape.getPathIterator(null); + final double[] pathCoordinate = new double[2]; + iterator.currentSegment(pathCoordinate); + coordinates.add(new CoordinateXY(pathCoordinate[0], pathCoordinate[1])); + iterator.next(); + while (!iterator.isDone()) { + iterator.currentSegment(pathCoordinate); + coordinates.add(new CoordinateXY(pathCoordinate[0], pathCoordinate[1])); + iterator.next(); + } + coordinates.add(coordinates.getFirst()); // close the polygon + return getGeometryFactory().createLinearRing(coordinates.toArray(Coordinate[]::new)); + } + + /** + * Converts a line2D to the jts geometry LinearString + * + * @param line2D line to convert + * @return LinearString geometry + */ + public static LineString line2DToLinearString(final Line2D line2D) { + return getGeometryFactory() + .createLineString( + new Coordinate[] { + GeometryUtil.point2DToCoordinate(line2D.getP1()), + GeometryUtil.point2DToCoordinate(line2D.getP2()) + }); + } + + /** + * Find the points of intersection between a line and a shape + * + * @param line the intersecting line + * @param shape the shape to intersect + * @return Array of intersecting points + */ + public static Point2D[] lineSegmentShapeIntersection(final Line2D line, final Shape shape) { + LineString lineString = line2DToLinearString(line); + LinearRing linearRing = shapeToLinearRing(shape); + Geometry intersection = lineString.intersection(linearRing); + if (intersection.getNumPoints() > 0) { + return Arrays.stream(intersection.getCoordinates()) + .map(GeometryUtil::coordinateToPoint2D) + .toArray(Point2D[]::new); + } + return new Point2D.Double[] {}; + } + public static Collection toJtsPolygons(Shape shape) { if (shape instanceof Area area && area.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java index ae45d0de35..9ea8ce996c 100644 --- a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java @@ -1831,7 +1831,7 @@ private void doDragTo(ZonePoint newAnchorPoint) { } // Don't bother if there isn't any movement - if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), newAnchorPoint)) { + if (renderer.isMoveSelectionSetUnchanged(tokenBeingDragged.getId(), newAnchorPoint)) { return; } diff --git a/src/main/java/net/rptools/maptool/client/tool/StampTool.java b/src/main/java/net/rptools/maptool/client/tool/StampTool.java index 9da5f9e942..89662cc690 100644 --- a/src/main/java/net/rptools/maptool/client/tool/StampTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/StampTool.java @@ -1206,7 +1206,7 @@ public void moveByKey(int dx, int dy, boolean micro) { private void doDragTo(ZonePoint newAnchorPoint) { // Don't bother if there isn't any movement - if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), newAnchorPoint)) { + if (renderer.isMoveSelectionSetUnchanged(tokenBeingDragged.getId(), newAnchorPoint)) { return; } diff --git a/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java index 6bacee5fac..f2258690c2 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java @@ -14,7 +14,6 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Rectangle; import net.rptools.maptool.model.Token; @@ -52,11 +51,6 @@ protected BooleanTokenOverlay(String aName) { @Override public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, Object value) { if (FunctionUtil.getBooleanValue(value)) { - // Apply Alpha Transparency - float opacity = token.getTokenOpacity(); - if (opacity < 1.0f) - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - paintOverlay(g, token, bounds); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/token/ColorDotTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/ColorDotTokenOverlay.java index 939044a5f1..13e24ed803 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/ColorDotTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/ColorDotTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; @@ -78,7 +76,6 @@ public Object clone() { public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { Color tempColor = g.getColor(); Stroke tempStroke = g.getStroke(); - Composite tempComposite = g.getComposite(); try { g.setColor(getColor()); g.setStroke(getStroke()); @@ -101,14 +98,10 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { break; } // endswitch Shape s = new Ellipse2D.Double(x, y, size, size); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.fill(s); } finally { g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/token/CrossTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/CrossTokenOverlay.java index 6e424262c4..b29ad7c474 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/CrossTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/CrossTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -74,17 +72,12 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(getColor()); Stroke tempStroke = g.getStroke(); g.setStroke(getStroke()); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.draw( new Line2D.Double(0, (double) bounds.height / 2, bounds.width, (double) bounds.height / 2)); g.draw( new Line2D.Double((double) bounds.width / 2, 0, (double) bounds.width / 2, bounds.height)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } public static CrossTokenOverlay fromDto(BooleanTokenOverlayDto dto) { diff --git a/src/main/java/net/rptools/maptool/client/ui/token/DiamondTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/DiamondTokenOverlay.java index 6faf5a9dd8..aef6f46e32 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/DiamondTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/DiamondTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -76,17 +74,13 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(getColor()); Stroke tempStroke = g.getStroke(); g.setStroke(getStroke()); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); + g.draw(new Line2D.Double(0, vc, hc, 0)); g.draw(new Line2D.Double(hc, 0, bounds.width, vc)); g.draw(new Line2D.Double(bounds.width, vc, hc, bounds.height)); g.draw(new Line2D.Double(hc, bounds.height, 0, vc)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } public static DiamondTokenOverlay fromDto(BooleanTokenOverlayDto dto) { diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java index c4558d5255..1326e82970 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; @@ -91,19 +89,14 @@ protected TokenOverlayFlow getFlow() { public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { Color tempColor = g.getColor(); Stroke tempStroke = g.getStroke(); - Composite tempComposite = g.getComposite(); try { g.setColor(getColor()); g.setStroke(getStroke()); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); Shape s = getShape(bounds, aToken); g.fill(s); } finally { g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java index 55e6fa546b..d18d7d82ee 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java @@ -89,12 +89,7 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds) { int height = size.height; int x = iBounds.x + (d.width - width) / 2; int y = iBounds.y + (d.height - height) / 2; - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.drawImage(image, x, y, size.width, size.height, null); - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/MultipleImageBarTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/MultipleImageBarTokenOverlay.java index cde12ee7b2..872638f636 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/MultipleImageBarTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/MultipleImageBarTokenOverlay.java @@ -14,8 +14,6 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; -import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; @@ -98,13 +96,7 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, double val y = d.height - size.height; } - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) { - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); - } g.drawImage(image, x, y, size.width, size.height, null); - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/OTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/OTokenOverlay.java index 00fc54da28..e720f76ded 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/OTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/OTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -73,17 +71,12 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(getColor()); Stroke tempStroke = g.getStroke(); g.setStroke(getStroke()); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); double offset = getStroke().getLineWidth() / 2.0; g.draw( new Ellipse2D.Double( 0 + offset, 0 + offset, bounds.width - offset * 2, bounds.height - offset * 2)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } public static OTokenOverlay fromDto(BooleanTokenOverlayDto dto) { diff --git a/src/main/java/net/rptools/maptool/client/ui/token/ShadedTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/ShadedTokenOverlay.java index ed904e503d..b392ef8e29 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/ShadedTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/ShadedTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import net.rptools.maptool.model.Token; @@ -71,13 +69,8 @@ public ShadedTokenOverlay(String aName, Color aColor) { public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { Color temp = g.getColor(); g.setColor(color); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.fill(bounds); g.setColor(temp); - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/SingleImageBarTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/SingleImageBarTokenOverlay.java index 3b621ac125..ed33976d82 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/SingleImageBarTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/SingleImageBarTokenOverlay.java @@ -14,8 +14,6 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; -import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; @@ -95,11 +93,6 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, double val y = d.height - size.height; } - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) { - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); - } int width = (getSide() == Side.TOP || getSide() == Side.BOTTOM) ? calcBarSize(image.getWidth(), value) @@ -129,7 +122,6 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, double val image.getWidth(), image.getHeight(), null); - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/TriangleTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/TriangleTokenOverlay.java index cf89173ecc..a470fa61a3 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/TriangleTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/TriangleTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -76,16 +74,11 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(getColor()); Stroke tempStroke = g.getStroke(); g.setStroke(getStroke()); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.draw(new Line2D.Double(0, vc, bounds.width, vc)); g.draw(new Line2D.Double(bounds.width, vc, hc, 0)); g.draw(new Line2D.Double(hc, 0, 0, vc)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } public static TriangleTokenOverlay fromDto(BooleanTokenOverlayDto dto) { diff --git a/src/main/java/net/rptools/maptool/client/ui/token/TwoImageBarTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/TwoImageBarTokenOverlay.java index 0a51d722e7..998bc15459 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/TwoImageBarTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/TwoImageBarTokenOverlay.java @@ -14,8 +14,6 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; -import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; @@ -102,11 +100,6 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, double val y = d.height - size.height; } - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); - int width = (getSide() == Side.TOP || getSide() == Side.BOTTOM) ? calcBarSize(images[0].getWidth(), value) @@ -141,7 +134,6 @@ public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, double val } else { g.drawImage(images[0], x, y, x + screenWidth, y + screenHeight, 0, 0, width, height, null); } - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java index dd490186db..b40faccfd9 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java @@ -14,10 +14,8 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -73,16 +71,10 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(color); Stroke tempStroke = g.getStroke(); g.setStroke(stroke); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) { - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); - } g.draw(new Line2D.Double(0, 0, bounds.width, bounds.height)); g.draw(new Line2D.Double(0, bounds.height, bounds.width, 0)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/token/YieldTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/YieldTokenOverlay.java index f810f6e5a8..1cf61113bc 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/YieldTokenOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/YieldTokenOverlay.java @@ -14,9 +14,7 @@ */ package net.rptools.maptool.client.ui.token; -import java.awt.AlphaComposite; import java.awt.Color; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; @@ -76,16 +74,11 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) { g.setColor(getColor()); Stroke tempStroke = g.getStroke(); g.setStroke(getStroke()); - Composite tempComposite = g.getComposite(); - if (getOpacity() != 100) - g.setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) getOpacity() / 100)); g.draw(new Line2D.Double(0, vc, bounds.width, vc)); g.draw(new Line2D.Double(bounds.width, vc, hc, bounds.height)); g.draw(new Line2D.Double(hc, bounds.height, 0, vc)); g.setColor(tempColor); g.setStroke(tempStroke); - g.setComposite(tempComposite); } public static YieldTokenOverlay fromDto(BooleanTokenOverlayDto dto) { diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneViewModel.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneViewModel.java index 778d5ca3b3..dcd2db4d5c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneViewModel.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneViewModel.java @@ -161,6 +161,10 @@ public Rectangle2D getViewport() { viewport.getMinX(), viewport.getMinY(), viewport.getWidth(), viewport.getHeight()); } + public boolean isUsingVision() { + return zoneView.isUsingVision(); + } + public Area getVisibleArea() { return visibleArea; } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/HaloRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/HaloRenderer.java index f48b5dbf56..cb66277222 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/HaloRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/HaloRenderer.java @@ -30,6 +30,13 @@ public class HaloRenderer { private final RenderHelper renderHelper; private final Zone zone; + private float opacity = AppPreferences.haloOverlayOpacity.get() / 255f; + private float lineWeight = AppPreferences.haloLineWidth.get(); + + { + AppPreferences.haloOverlayOpacity.onChange(i -> opacity = i / 255f); + AppPreferences.haloLineWidth.onChange(i -> lineWeight = i); + } // region These fields need to be recalculated whenever the grid changes. @@ -69,16 +76,15 @@ private Shape getHaloShape(Grid grid) { .createTransformedShape(cachedHaloShape); } } - return cachedHaloShape; } // Render Halos - public void renderHalo(Graphics2D g2d, Token token, ZoneViewModel.TokenPosition position) { + public void renderHalo(Graphics2D g2d, ZoneViewModel.TokenPosition position) { + Token token = position.token(); if (token.getHaloColor() == null) { return; } - var grid = zone.getGrid(); if (grid == null) { return; @@ -105,29 +111,32 @@ public void renderHalo(Graphics2D g2d, Token token, ZoneViewModel.TokenPosition position.transformedBounds().getBounds2D().getCenterX(), position.transformedBounds().getBounds2D().getCenterY()) .createTransformedShape(paintShape); - // this will eventually hold forks for painting different types of halo renderHelper.render( g2d, worldG -> { - paintLineHalo(worldG, token, grid, positionedPaintShape); + paintLineHalo(worldG, position.token(), grid, positionedPaintShape); }); } private void paintLineHalo(Graphics2D g2d, Token token, Grid grid, Shape paintShape) { + Stroke oldStroke = g2d.getStroke(); + g2d.setColor(token.getHaloColor()); // double width because we will clip the inside half g2d.setStroke( new BasicStroke( - (float) - (2f - * Math.min(1f, token.getFootprint(grid).getScale()) - * AppPreferences.haloLineWidth.get()))); - g2d.setColor(token.getHaloColor()); + (float) (2f * lineWeight * Math.min(1f, token.getFootprint(grid).getScale())))); Shape oldClip = g2d.getClip(); + Composite oldComposite = g2d.getComposite(); + if (opacity < 1f) { + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); + } Area a = new Area(g2d.getClipBounds()); a.subtract(new Area(paintShape)); g2d.setClip(a); g2d.draw(paintShape); g2d.setClip(oldClip); + g2d.setComposite(oldComposite); + g2d.setStroke(oldStroke); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ItemRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ItemRenderer.java index 9fa98abd4e..4b31ac4c0b 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ItemRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ItemRenderer.java @@ -16,7 +16,7 @@ import java.awt.*; -interface ItemRenderer { +public interface ItemRenderer { public void render(Graphics2D g); } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/LabelRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/LabelRenderer.java index 598e0c4be6..e3f1aa85a6 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/LabelRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/LabelRenderer.java @@ -16,18 +16,34 @@ import java.awt.*; import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; import javax.swing.*; +import net.rptools.maptool.client.AppState; +import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.swing.ImageLabel; +import net.rptools.maptool.client.ui.zone.ZoneViewModel; import net.rptools.maptool.model.GUID; import net.rptools.maptool.util.GraphicsUtil; /** Represents a delayed label render */ -class LabelRenderer implements ItemRenderer { +public class LabelRenderer implements ItemRenderer { + + private static final Map labelCache = new HashMap<>(); + private static final Map labelImageCache = new HashMap<>(); + + public static Map getLabelCache() { + return labelCache; + } + + public static Map getLabelImageCache() { + return labelImageCache; + } private final ZoneRenderer renderer; private final String text; private int x; - private final int y; + private int y; private final int align; private final Color foreground; private final ImageLabel background; @@ -52,8 +68,8 @@ public LabelRenderer(ZoneRenderer renderer, String text, int x, int y, GUID tId) this.foreground = Color.black; tokenId = tId; if (tokenId != null) { - width = renderer.labelRenderingCache.get(tokenId).getWidth(); - height = renderer.labelRenderingCache.get(tokenId).getHeight(); + width = labelImageCache.get(tokenId).getWidth(); + height = labelImageCache.get(tokenId).getHeight(); } } @@ -87,11 +103,22 @@ public LabelRenderer( this.background = background; tokenId = tId; if (tokenId != null) { - width = renderer.labelRenderingCache.get(tokenId).getWidth(); - height = renderer.labelRenderingCache.get(tokenId).getHeight(); + width = labelImageCache.get(tokenId).getWidth(); + height = labelImageCache.get(tokenId).getHeight(); } } + public static boolean isLabelVisible( + ZoneViewModel.TokenPosition position, ZoneViewModel viewModel, boolean hover) { + if (!(AppState.isShowTokenNames() || hover)) { + return false; + } + // if policy does not auto-reveal FoW, check if fog covers the token (slow) + return viewModel.getPlayerView().isGMView() + || (viewModel.isUsingVision() && MapTool.getServerPolicy().isAutoRevealOnMovement()) + || viewModel.zone.isTokenVisible(position.token()); + } + public void render(Graphics2D g) { if (tokenId != null) { // Use cached image. switch (align) { @@ -104,7 +131,7 @@ public void render(Graphics2D g) { case SwingUtilities.LEFT: break; } - BufferedImage img = renderer.labelRenderingCache.get(tokenId); + BufferedImage img = labelImageCache.get(tokenId); if (img != null) { g.drawImage(img, x, y, width, height, null); } else { // Draw as normal @@ -114,4 +141,20 @@ public void render(Graphics2D g) { GraphicsUtil.drawBoxedString(g, text, x, y, align, background, foreground); } } + + public void setHeight(int height) { + this.height = height; + } + + public void setWidth(int width) { + this.width = width; + } + + public void setX(int x) { + this.x = x; + } + + public void setY(int y) { + this.y = y; + } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java index b7c49a7658..dd0e0547a2 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java @@ -28,6 +28,7 @@ import java.awt.font.TextLayout; import java.awt.geom.*; import java.awt.image.BufferedImage; +import java.io.Serial; import java.text.NumberFormat; import java.util.*; import java.util.List; @@ -51,12 +52,9 @@ import net.rptools.maptool.client.ui.Scale; import net.rptools.maptool.client.ui.theme.Images; import net.rptools.maptool.client.ui.theme.RessourceManager; -import net.rptools.maptool.client.ui.token.AbstractTokenOverlay; -import net.rptools.maptool.client.ui.token.BarTokenOverlay; import net.rptools.maptool.client.ui.token.dialog.create.NewTokenDialog; import net.rptools.maptool.client.ui.zone.*; import net.rptools.maptool.client.ui.zone.gdx.GdxRenderer; -import net.rptools.maptool.client.ui.zone.renderer.tokenRender.FacingArrowRenderer; import net.rptools.maptool.client.ui.zone.renderer.tokenRender.TokenRenderer; import net.rptools.maptool.client.walker.ZoneWalker; import net.rptools.maptool.events.MapToolEventBus; @@ -69,16 +67,17 @@ import net.rptools.maptool.model.zones.*; import net.rptools.maptool.util.GraphicsUtil; import net.rptools.maptool.util.ImageManager; -import net.rptools.maptool.util.ImageSupport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** */ public class ZoneRenderer extends JComponent implements DropTargetListener { - private static final long serialVersionUID = 3832897780066104884L; + @Serial private static final long serialVersionUID = 3832897780066104884L; private static final Logger log = LogManager.getLogger(ZoneRenderer.class); + private static final FlatImageLabelFactory IMAGE_LABEL_FACTORY = new FlatImageLabelFactory(); + /** DebounceExecutor for throttling repaint() requests. */ private final DebounceExecutor repaintDebouncer; @@ -109,9 +108,7 @@ public class ZoneRenderer extends JComponent implements DropTargetListener { private final List showPathList = new ArrayList<>(); // Optimizations - final Map labelRenderingCache = new HashMap<>(); private Token tokenUnderMouse; - private ScreenPoint pointUnderMouse; private @Nonnull Zone.Layer activeLayer = Layer.getDefaultPlayerLayer(); @@ -134,9 +131,7 @@ public class ZoneRenderer extends JComponent implements DropTargetListener { private final EnumSet disabledLayers = EnumSet.noneOf(Layer.class); private final GridRenderer gridRenderer; - private final HaloRenderer haloRenderer; private final TokenRenderer tokenRenderer; - private final FacingArrowRenderer facingArrowRenderer; private final SelectionRenderer selectionRenderer; private final LightsRenderer lightsRenderer; private final DarknessRenderer darknessRenderer; @@ -159,20 +154,19 @@ public ZoneRenderer(Zone zone) { throw new IllegalArgumentException("Zone cannot be null"); } this.zone = zone; - selectionModel = new SelectionModel(zone); - zoneView = new ZoneView(zone); + this.selectionModel = new SelectionModel(zone); + this.zoneView = new ZoneView(zone); this.viewModel = new ZoneViewModel(zone, zoneView, selectionModel); setZoneScale(new Scale()); - drawableRenderers = + this.drawableRenderers = CollectionUtil.newFilledEnumMap( Zone.Layer.class, layer -> new PartitionedDrawableRenderer(zone)); var renderHelper = new RenderHelper(this, tempBufferPool); this.gridRenderer = new GridRenderer(this); - this.haloRenderer = new HaloRenderer(renderHelper, zone); + this.tokenRenderer = new TokenRenderer(renderHelper, zone); - this.facingArrowRenderer = new FacingArrowRenderer(renderHelper, zone); this.selectionRenderer = new SelectionRenderer(renderHelper, viewModel, zoneView); this.lightsRenderer = new LightsRenderer(renderHelper, zone, zoneView); this.darknessRenderer = new DarknessRenderer(renderHelper, zoneView); @@ -180,7 +174,7 @@ public ZoneRenderer(Zone zone) { this.fogRenderer = new FogRenderer(renderHelper, zone, zoneView); this.visionOverlayRenderer = new VisionOverlayRenderer(renderHelper, zone, zoneView); this.debugRenderer = new DebugRenderer(renderHelper); - repaintDebouncer = + this.repaintDebouncer = new DebounceExecutor(1000 / AppPreferences.frameRateCap.get(), this::repaint); setFocusable(true); @@ -301,7 +295,7 @@ public void setMouseOver(Token token) { if (tokenUnderMouse == token) { return; } - tokenUnderMouse = token; + this.tokenUnderMouse = token; repaintDebouncer.dispatch(); } @@ -324,13 +318,13 @@ public void addMoveSelectionSet(String playerId, GUID keyToken, Set tokenL return set.getKeyTokenDragAnchorPosition(); } - public boolean hasMoveSelectionSetMoved(GUID keyToken, ZonePoint dragAnchorPosition) { + public boolean isMoveSelectionSetUnchanged(GUID keyToken, ZonePoint dragAnchorPosition) { SelectionSet set = selectionSetMap.get(keyToken); if (set == null) { - return false; + return true; } - return !set.getKeyTokenDragAnchorPosition().equals(dragAnchorPosition); + return set.getKeyTokenDragAnchorPosition().equals(dragAnchorPosition); } public void updateMoveSelectionSet(GUID keyToken, ZonePoint latestPoint) { @@ -554,15 +548,13 @@ public void centerOn(CellPoint point) { } /** - * Remove the token from: {@link #labelRenderingCache}. Set the {@link #visibleScreenArea} to - * null. Flush the token from {@link #zoneView}. + * Set the {@link #visibleScreenArea} to null. Flush the token from {@link #zoneView}. * * @param token the token to flush */ public void flush(Token token) { // This method can be called from a non-EDT thread so if that happens, make sure we synchronize // with the EDT. - labelRenderingCache.remove(token.getId()); // This should be smarter, but whatever visibleScreenArea = null; @@ -684,8 +676,8 @@ public void enforceView(int x, int y, double scale, int gmWidth, int gmHeight) { } public void restoreView() { - log.info("Restoring view: " + previousZonePoint); - log.info("previousScale: " + previousScale); + log.info("Restoring view: {}", previousZonePoint); + log.info("previousScale: {}", previousScale); centerOn(previousZonePoint); setScale(previousScale); @@ -987,7 +979,7 @@ public void renderZone(Graphics2D g2d, @Nullable PlayerView view) { // (This method has its own 'timer' calls) if (AppState.getShowTextLabels()) { - renderLabels(g2d, view); + renderMapLabels(g2d, view); } this.fogRenderer.render(g2d, view); @@ -1018,12 +1010,10 @@ public void renderZone(Graphics2D g2d, @Nullable PlayerView view) { showBlockedMoves(g2d, view, getOwnedMovementSet(view)); timer.stop("owned movement"); - // Text associated with tokens being moved is added to a list to be drawn after, i.e. on top - // of, the tokens themselves. - // So if one moving token is on top of another moving token, at least the textual identifiers - // will be visible. + // To ensure text associated with moving tokens is visible we add it to the delayed render + // list last so it is painted on top of other features. timer.start("token name/labels"); - renderRenderables(g2d); + renderDelayedPaintRenderables(g2d); timer.stop("token name/labels"); } @@ -1053,10 +1043,12 @@ public void renderZone(Graphics2D g2d, @Nullable PlayerView view) { } private void delayRendering(ItemRenderer renderer) { - itemRenderList.add(renderer); + if (!itemRenderList.contains(renderer)) { + itemRenderList.add(renderer); + } } - private void renderRenderables(Graphics2D g) { + private void renderDelayedPaintRenderables(Graphics2D g) { for (ItemRenderer renderer : itemRenderList) { renderer.render(g); } @@ -1070,7 +1062,7 @@ private void renderRenderables(Graphics2D g) { */ private final BufferedImagePool tempBufferPool = new BufferedImagePool(2); - private void renderLabels(Graphics2D g, PlayerView view) { + private void renderMapLabels(Graphics2D g, PlayerView view) { final var timer = CodeTimer.get(); timer.start("labels-1"); @@ -1087,7 +1079,7 @@ private void renderLabels(Graphics2D g, PlayerView view) { var dim = fLabel.getDimensions(g, label.getLabel()); Rectangle bounds = fLabel.render( - g, (int) (sp.x - dim.width / 2), (int) (sp.y - dim.height / 2), label.getLabel()); + g, (int) (sp.x - dim.width / 2d), (int) (sp.y - dim.height / 2d), label.getLabel()); labelLocationList.add(new LabelLocation(bounds, label)); timer.stop("labels-1.1"); } @@ -1269,7 +1261,7 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set newArea.transform(AffineTransform.getTranslateInstance(set.getOffsetX(), set.getOffsetY())); var newPosition = new ZoneViewModel.TokenPosition(token, newBounds, newArea); - tokenRenderer.renderToken(token, newPosition, g, 1); + tokenRenderer.renderToken(token, viewModel, newPosition, g, true, true, false); // Other details. // Only draw these if the token is visible on screen where it is dragged to. @@ -1533,8 +1525,8 @@ public void renderPath( } p = new ZonePoint( - (int) (p.x + (footprintBounds.width / 2) * footprint.getScale()), - (int) (p.y + (footprintBounds.height / 2) * footprint.getScale())); + (int) (p.x + (footprintBounds.width / 2d) * footprint.getScale()), + (int) (p.y + (footprintBounds.height / 2d) * footprint.getScale())); highlightCell(g, p, RessourceManager.getImage(Images.ZONE_RENDERER_CELL_WAYPOINT), .333f); } timer.stop("renderPath-3"); @@ -1733,7 +1725,6 @@ protected void renderTokens( final var timer = CodeTimer.get(); Graphics2D clippedG = g; - var imageLabelFactory = new FlatImageLabelFactory(); boolean isGMView = view.isGMView(); // speed things up @@ -1766,12 +1757,10 @@ protected void renderTokens( position = viewModel.getTokenPositions().get(token.getId()); if (position == null) { - // Unknown token? - continue; + continue; // Unknown token? } if (!viewModel.getVisibleTokens(token.getLayer()).contains(token.getId())) { - // Token not on screen or otherwise not visible. - continue; + continue; // Token not on screen or otherwise not visible. } } finally { timer.stop("token-list-1"); @@ -1788,8 +1777,10 @@ protected void renderTokens( } } else { tokenG = (Graphics2D) g.create(); - AppPreferences.renderQuality.get().setRenderingHints(tokenG); } + AppPreferences.renderQuality.get().setRenderingHints(tokenG); + + boolean isHover = token == tokenUnderMouse; // Previous path timer.start("renderTokens:ShowPath"); @@ -1798,118 +1789,12 @@ protected void renderTokens( } timer.stop("renderTokens:ShowPath"); - timer.start("token-list-1b"); - // get token image, using image table if present - BufferedImage image = ImageSupport.getTokenImage(token, this); - timer.stop("token-list-1b"); - - timer.start("token-list-5a"); - if (token.getIsFlippedIso() && getZone().getGrid().isIsometric()) { - int newSize = (image.getWidth() + image.getHeight()); - token.setWidth(newSize); - token.setHeight(newSize / 2); - } - timer.stop("token-list-5a"); - - // Render Halo - haloRenderer.renderHalo(tokenG, token, position); - - // Calculate alpha Transparency from token and use opacity to indicate that token is moving - float opacity = token.getTokenOpacity(); - if (viewModel.isTokenMoving(token.getId())) { - opacity = opacity / 2.0f; - } - // Finally render the token image - timer.start("token-list-7"); - // Clipping is handled in the isTokenInNeedOfClipping() call far above. - tokenRenderer.renderToken(token, position, tokenG, opacity); - timer.stop("token-list-7"); - - timer.start("token-list-8"); - // Facing - facingArrowRenderer.paintArrow(tokenG, position); - timer.stop("token-list-8"); - - timer.start("token-list-9"); - // Set up the graphics so that the overlay can just be painted. - Rectangle2D tokenBounds = zoneScale.toScreenSpace(position.transformedBounds().getBounds2D()); - Graphics2D locG = - (Graphics2D) - tokenG.create( - (int) tokenBounds.getX(), - (int) tokenBounds.getY(), - (int) tokenBounds.getWidth(), - (int) tokenBounds.getHeight()); - Rectangle bounds = - new Rectangle(0, 0, (int) tokenBounds.getWidth(), (int) tokenBounds.getHeight()); - - // Check each of the set values - for (String state : MapTool.getCampaign().getTokenStatesMap().keySet()) { - Object stateValue = token.getState(state); - AbstractTokenOverlay overlay = MapTool.getCampaign().getTokenStatesMap().get(state); - if (stateValue instanceof AbstractTokenOverlay) { - overlay = (AbstractTokenOverlay) stateValue; - } - if (overlay == null - || overlay.isMouseover() && token != tokenUnderMouse - || !overlay.showPlayer(token, MapTool.getPlayer())) { - continue; - } - overlay.paintOverlay(locG, token, bounds, stateValue); - } - timer.stop("token-list-9"); - - timer.start("token-list-10"); - - for (String bar : MapTool.getCampaign().getTokenBarsMap().keySet()) { - Object barValue = token.getState(bar); - BarTokenOverlay overlay = MapTool.getCampaign().getTokenBarsMap().get(bar); - if (overlay == null - || overlay.isMouseover() && token != tokenUnderMouse - || !overlay.showPlayer(token, MapTool.getPlayer())) { - continue; - } - - overlay.paintOverlay(locG, token, bounds, barValue); - } - locG.dispose(); - timer.stop("token-list-10"); - - timer.start("token-list-11"); - // Keep track of which tokens have been drawn for post-processing on them later - // (such as selection borders and names/labels) - if (getActiveLayer().equals(token.getLayer())) { - tokenPostProcessing.add(position); - } - timer.stop("token-list-11"); - } - - // Selection and labels - timer.start("token-list-12"); - for (ZoneViewModel.TokenPosition position : tokenPostProcessing) { - var token = position.token(); - - // Count moving tokens as "selected" so that a border is drawn around them. - boolean isSelected = - selectionModel.isSelected(token.getId()) || viewModel.isTokenMoving(token.getId()); - if (isSelected) { - selectionRenderer.drawSelectBorder(clippedG, position); - // Remove labels from the cache if the corresponding tokens are deselected - } else if (!AppState.isShowTokenNames()) { - labelRenderingCache.remove(token.getId()); - } - - // Token names and labels - boolean showCurrentTokenLabel = AppState.isShowTokenNames() || token == tokenUnderMouse; - - // if policy does not auto-reveal FoW, check if fog covers the token (slow) - if (showCurrentTokenLabel - && !isGMView - && (!zoneView.isUsingVision() || !MapTool.getServerPolicy().isAutoRevealOnMovement()) - && !zone.isTokenVisible(token)) { - showCurrentTokenLabel = false; - } + timer.start("renderTokens:LabelCheck"); + // Token name and label + boolean showCurrentTokenLabel = LabelRenderer.isLabelVisible(position, viewModel, isHover); + timer.stop("renderTokens:LabelCheck"); if (showCurrentTokenLabel) { + timer.start("renderTokens:LabelBuild"); GUID tokId = token.getId(); int offset = 3; // Keep it from tramping on the token border. ImageLabel background; @@ -1931,10 +1816,10 @@ protected void renderTokens( if (isGMView && token.getGMName() != null && !StringUtil.isEmpty(token.getGMName())) { name += " (" + token.getGMName() + ")"; } - if (!view.equals(lastView) || !labelRenderingCache.containsKey(tokId)) { + if (!view.equals(lastView) || !LabelRenderer.getLabelImageCache().containsKey(tokId)) { boolean hasLabel = false; - var flatImgLabel = imageLabelFactory.getMapImageLabel(token); + var flatImgLabel = IMAGE_LABEL_FACTORY.getMapImageLabel(token); var nameDimension = flatImgLabel.getDimensions(g, name); var labelDimension = new Dimension(0, 0); @@ -1958,23 +1843,59 @@ protected void renderTokens( token.getLabel()); } flatImgLabel.render(gLabelRender, (width - nameDimension.width) / 2, 0, name); - // Add image to cache - labelRenderingCache.put(tokId, labelRender); + LabelRenderer.getLabelImageCache().put(tokId, labelRender); } // Create LabelRenderer using cached label. - Rectangle r = - zoneScale.toScreenSpace(position.transformedBounds().getBounds2D()).getBounds(); - delayRendering( - new LabelRenderer( - this, - name, - r.x + r.width / 2, - r.y + r.height + offset, - SwingUtilities.CENTER, - background, - foreground, - tokId)); + Rectangle r = zoneScale.toScreenSpace(position.footprintBounds().getBounds2D()).getBounds(); + LabelRenderer label = LabelRenderer.getLabelCache().get(tokId); + if (label != null) { + label.setX(r.x + r.width / 2); + label.setY(r.y + r.height + offset); + } else { + label = + new LabelRenderer( + this, + name, + r.x + r.width / 2, + r.y + r.height + offset, + SwingUtilities.CENTER, + background, + foreground, + tokId); + } + LabelRenderer.getLabelCache().put(tokId, label); + delayRendering(label); + timer.stop("renderTokens:LabelBuild"); + } else { + LabelRenderer.getLabelCache().remove(token.getId()); + } + + // Render the token image and decorations + timer.start("token-list-7"); + // Clipping is handled in the isTokenInNeedOfClipping() call far above. + boolean isSelected = selectionModel.isSelected(token.getId()); + boolean isMoving = viewModel.isTokenMoving(token.getId()); + + tokenRenderer.renderToken(token, viewModel, position, tokenG, isSelected, isMoving, isHover); + tokenG.dispose(); + timer.stop("token-list-7"); + + timer.start("token-list-11"); + // Keep track of which tokens have been drawn for post-processing selection border + if (getActiveLayer().equals(token.getLayer())) { + tokenPostProcessing.add(position); + } + timer.stop("token-list-11"); + } + + // Selection + timer.start("token-list-12"); + for (ZoneViewModel.TokenPosition position : tokenPostProcessing) { + var token = position.token(); + // Count moving tokens as "selected" so that a border is drawn around them. + if (selectionModel.isSelected(token.getId()) || viewModel.isTokenMoving(token.getId())) { + selectionRenderer.drawSelectBorder(clippedG, position); } } timer.stop("token-list-12"); @@ -2470,7 +2391,7 @@ sure the current Player owns the token being duplicated (to avoid subtle ways of for (MD5Key id : token.getAllImageAssets()) { Asset asset = AssetManager.getAsset(id); if (asset == null) { - log.error("Could not find image for asset: " + id); + log.error("Could not find image for asset: {}", id); continue; } MapToolUtil.uploadAsset(asset); @@ -2544,6 +2465,7 @@ public List getVisibleTokens() { @Override public void dropActionChanged(DropTargetDragEvent dtde) {} + @SuppressWarnings("unused") @Subscribe private void onSelectionChanged(SelectionModel.SelectionChanged event) { if (event.zone() != zone) { @@ -2554,6 +2476,7 @@ private void onSelectionChanged(SelectionModel.SelectionChanged event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onTokensAdded(TokensAdded event) { if (event.zone() != this.zone) { @@ -2567,6 +2490,7 @@ private void onTokensAdded(TokensAdded event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onTokensRemoved(TokensRemoved event) { if (event.zone() != this.zone) { @@ -2580,6 +2504,7 @@ private void onTokensRemoved(TokensRemoved event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onTokensChanged(TokensChanged event) { if (event.zone() != this.zone) { @@ -2593,6 +2518,7 @@ private void onTokensChanged(TokensChanged event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onFogChanged(FogChanged event) { if (event.zone() != this.zone) { @@ -2611,6 +2537,7 @@ private void onTopologyChanged() { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onTopologyChanged(WallTopologyChanged event) { if (event.zone() != this.zone) { @@ -2619,6 +2546,7 @@ private void onTopologyChanged(WallTopologyChanged event) { onTopologyChanged(); } + @SuppressWarnings("unused") @Subscribe private void onTopologyChanged(MaskTopologyChanged event) { if (event.zone() != this.zone) { @@ -2631,6 +2559,7 @@ private void markDrawableLayerDirty(Layer layer) { drawableRenderers.get(layer).setDirty(); } + @SuppressWarnings("unused") @Subscribe private void onDrawableAdded(DrawableAdded event) { if (event.zone() != this.zone) { @@ -2641,6 +2570,7 @@ private void onDrawableAdded(DrawableAdded event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onDrawableRemoved(DrawableRemoved event) { if (event.zone() != this.zone) { @@ -2651,6 +2581,7 @@ private void onDrawableRemoved(DrawableRemoved event) { repaintDebouncer.dispatch(); } + @SuppressWarnings("unused") @Subscribe private void onBoardChanged(BoardChanged event) { if (event.zone() != this.zone) { @@ -2660,6 +2591,7 @@ private void onBoardChanged(BoardChanged event) { } // Should this be moved to GridRenderer? No. Lots of things depend on the grid. + @SuppressWarnings("unused") @Subscribe private void onGridChanged(GridChanged event) { if (event.zone() != this.zone) { diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java index 5b0aeef38f..050edf177e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java @@ -14,17 +14,20 @@ */ package net.rptools.maptool.client.ui.zone.renderer.tokenRender; +import com.google.common.eventbus.Subscribe; import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; +import java.awt.geom.*; import java.util.ArrayList; import net.rptools.lib.CodeTimer; +import net.rptools.lib.GeometryUtil; import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.ui.zone.ZoneViewModel.TokenPosition; import net.rptools.maptool.client.ui.zone.renderer.RenderHelper; +import net.rptools.maptool.model.GridFactory; import net.rptools.maptool.model.Token.TokenShape; import net.rptools.maptool.model.Zone; +import net.rptools.maptool.model.zones.GridChanged; +import net.rptools.maptool.util.GraphicsUtil; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; @@ -45,11 +48,29 @@ public class FacingArrowRenderer { } private final RenderHelper renderHelper; - private final Zone zone; + private Zone zone; private final ArrayList figureFillColours = new ArrayList<>(); + private final Color fillColour = Color.YELLOW; private final Color borderColour = Color.DARK_GRAY; + private boolean isIsometric; + private boolean isSquare; + + @SuppressWarnings("unused") + @Subscribe + private void onGridChanged(GridChanged event) { + if (event.zone() != null) { + this.zone = event.zone(); + if (zone.getGrid() == null) { + isIsometric = false; + isSquare = false; + } else { + isIsometric = this.zone.getGrid().isIsometric(); + isSquare = GridFactory.getGridType(this.zone.getGrid()).equals(GridFactory.SQUARE); + } + } + } public FacingArrowRenderer(RenderHelper renderHelper, Zone zone) { this.renderHelper = renderHelper; @@ -60,9 +81,16 @@ public FacingArrowRenderer(RenderHelper renderHelper, Zone zone) { for (int i = 89; i >= 0; i--) { figureFillColours.add(figureFillColours.get(i)); } + if (zone.getGrid() == null) { + isIsometric = false; + isSquare = false; + } else { + isIsometric = this.zone.getGrid().isIsometric(); + isSquare = GridFactory.getGridType(this.zone.getGrid()).equals(GridFactory.SQUARE); + } } - public void paintArrow(Graphics2D tokenG, TokenPosition position) { + public void paintArrow(Graphics2D g2d, TokenPosition position) { var timer = CodeTimer.get(); var token = position.token(); var tokenShape = token.getShape(); @@ -83,8 +111,10 @@ public void paintArrow(Graphics2D tokenG, TokenPosition position) { timer.stop("FacingArrowRenderer-preCheck"); timer.start("FacingArrowRenderer-render"); + // set the stroke to shrink for tiny tokens to prevent it crowding out the fill + g2d.setStroke(new BasicStroke((float) (0.85f * position.token().getSizeScale()))); renderHelper.render( - tokenG, + g2d, worldG -> paintArrowWorld(worldG, token.getFacing(), tokenShape, position.footprintBounds())); timer.stop("FacingArrowRenderer-render"); @@ -95,8 +125,6 @@ private void paintArrowWorld( var timer = CodeTimer.get(); timer.start("FacingArrowRenderer-paintArrow"); try { - final var isIsometric = zone.getGrid().isIsometric(); - timer.start("FacingArrowRenderer-calculateTransform"); int angle = Math.floorMod(facing + (isIsometric ? 45 : 0), 360); AffineTransform transform = @@ -107,49 +135,67 @@ private void paintArrowWorld( Shape facingArrow = transform.createTransformedShape(UNIT_ARROW); timer.stop("FacingArrowRenderer-transformArrow"); - timer.start("FacingArrowRenderer-fill"); + // draw first so that fill is always visible + tokenG.setColor(borderColour); + tokenG.draw(facingArrow); + if (TokenShape.FIGURE.equals(tokenShape) && angle <= 180) { tokenG.setColor(figureFillColours.get(angle)); } else { tokenG.setColor(fillColour); } tokenG.fill(facingArrow); - timer.stop("FacingArrowRenderer-fill"); - - timer.start("FacingArrowRenderer-draw"); - tokenG.setColor(borderColour); - tokenG.draw(facingArrow); - timer.stop("FacingArrowRenderer-draw"); } catch (Exception e) { log.error("Failed to paint facing arrow.", e); - } finally { - timer.stop("FacingArrowRenderer-paintArrow"); } + timer.stop("FacingArrowRenderer-paintArrow"); } - private static AffineTransform buildArrowTransform( + private AffineTransform buildArrowTransform( TokenShape shape, Rectangle2D footprintBounds, int angle, boolean isIsometric) { double radFacing = Math.toRadians(angle); AffineTransform transform = new AffineTransform(); + // move to footprint centre transform.translate(footprintBounds.getCenterX(), footprintBounds.getCenterY()); if (isIsometric) { transform.scale(1.0, 0.5); } + // spin to face correct direction. Not linear for isometric transform.rotate(-radFacing); - double distanceToPoint = footprintBounds.getWidth() / 2; - if (TokenShape.SQUARE.equals(shape) && !isIsometric) { - if (angle >= 45 && angle <= 135 || angle >= 225 && angle <= 315) { // Top or bottom face. - distanceToPoint = footprintBounds.getHeight() / 2 / Math.abs(Math.sin(radFacing)); - } else { // Left or right face - distanceToPoint = footprintBounds.getWidth() / 2 / Math.abs(Math.cos(radFacing)); - } + // calculate distance to edge + double distanceToPoint; + Shape cellShape = this.zone.getGrid().getCellShape(); + if (cellShape != null) { + Point2D centre = new Point2D.Double(0, 0); + // centre the cell shape + cellShape = + AffineTransform.getTranslateInstance( + -cellShape.getBounds2D().getCenterX(), -cellShape.getBounds2D().getCenterY()) + .createTransformedShape(cellShape); + double scale = footprintBounds.getWidth() / cellShape.getBounds2D().getWidth(); + // size the cell shape to the footprint - compensate for previous isometric scaling + cellShape = + AffineTransform.getScaleInstance(scale, isIsometric ? 2 * scale : scale) + .createTransformedShape(cellShape); + // create a line from the centre with token facing angle + Point2D farPoint = GraphicsUtil.getPointAtVector(centre, angle, 300 * scale); + Line2D.Double ray = new Line2D.Double(centre, farPoint); + // obtain the point the line intersects the cell shape + Point2D[] point2D = GeometryUtil.lineSegmentShapeIntersection(ray, cellShape); + distanceToPoint = Math.hypot(point2D[0].getX(), point2D[0].getY()); + } else { + // fallback for gridless, just use radius based on size + distanceToPoint = footprintBounds.getWidth() / 2; } + // move out to edge transform.translate(distanceToPoint, 0); - var size = footprintBounds.getWidth() / 2d; - transform.scale(size, size); + var sizeW = footprintBounds.getWidth() / 2d; + var sizeH = footprintBounds.getHeight() / 2d; + // make it look big + transform.scale(sizeW, sizeH); return transform; } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/StateRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/StateRenderer.java new file mode 100644 index 0000000000..ee883efb67 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/StateRenderer.java @@ -0,0 +1,113 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.zone.renderer.tokenRender; + +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.util.*; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ui.token.*; +import net.rptools.maptool.client.ui.zone.ZoneViewModel; +import net.rptools.maptool.client.ui.zone.renderer.RenderHelper; +import net.rptools.maptool.model.Zone; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +public class StateRenderer { + private static final Logger log = LogManager.getLogger(StateRenderer.class); + private final RenderHelper renderHelper; + private final Zone zone; + private final Map barMap = + Collections.synchronizedMap(MapTool.getCampaign().getTokenBarsMap()); + private final Map stateMap = + Collections.synchronizedMap(MapTool.getCampaign().getTokenStatesMap()); + private Set overlayNames; + private boolean isPaintBars = false; + + public StateRenderer(RenderHelper renderHelper, Zone zone) { + this.renderHelper = renderHelper; + this.zone = zone; + } + + public void renderStates( + ZoneViewModel viewModel, + ZoneViewModel.TokenPosition position, + Graphics2D g2d, + boolean selected, + boolean hover) { + isPaintBars = false; + synchronized (stateMap) { + overlayNames = stateMap.keySet(); + } + renderOverlay(viewModel, position, g2d, selected, hover); + } + + public void renderBars( + ZoneViewModel viewModel, + ZoneViewModel.TokenPosition position, + Graphics2D g2d, + boolean selected, + boolean hover) { + isPaintBars = true; + synchronized (barMap) { + overlayNames = barMap.keySet(); + } + renderOverlay(viewModel, position, g2d, selected, hover); + } + + @SuppressWarnings("unused") + public void renderOverlay( + ZoneViewModel viewModel, + ZoneViewModel.TokenPosition position, + Graphics2D g2d, + boolean selected, + boolean hover) { + Rectangle2D tokenBounds = + viewModel.getZoneScale().toScreenSpace(position.footprintBounds().getBounds2D()); + Rectangle bounds = + new Rectangle(0, 0, (int) tokenBounds.getWidth(), (int) tokenBounds.getHeight()); + Graphics2D overlayG = + (Graphics2D) + g2d.create( + (int) tokenBounds.getX(), + (int) tokenBounds.getY(), + (int) tokenBounds.getWidth(), + (int) tokenBounds.getHeight()); + overlayG.setClip(null); + + Composite oldComposite = g2d.getComposite(); + float alpha = 1f; + if (oldComposite instanceof AlphaComposite alphaComposite) { + alpha = alphaComposite.getAlpha(); + } + + // Check each of the set values + for (String name : overlayNames) { + AbstractTokenOverlay overlay = isPaintBars ? barMap.get(name) : stateMap.get(name); + Object value = position.token().getState(name); + if (overlay == null + || overlay.isMouseover() && hover + || !overlay.showPlayer(position.token(), MapTool.getPlayer())) { + continue; + } + overlayG.setComposite( + AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, alpha * (float) overlay.getOpacity() / 100)); + + overlay.paintOverlay(overlayG, position.token(), bounds, value); + } + overlayG.dispose(); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenDecorationRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenDecorationRenderer.java new file mode 100644 index 0000000000..f1c7a5d798 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenDecorationRenderer.java @@ -0,0 +1,90 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.zone.renderer.tokenRender; + +import java.awt.*; +import net.rptools.lib.CodeTimer; +import net.rptools.maptool.client.ui.zone.ZoneViewModel; +import net.rptools.maptool.client.ui.zone.renderer.HaloRenderer; +import net.rptools.maptool.client.ui.zone.renderer.RenderHelper; +import net.rptools.maptool.model.Zone; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +public class TokenDecorationRenderer { + /* Render order by increasing z-index + AURA, + HALO, + TOKEN, + STATE, + BAR, + FACING, + LABEL; + */ + private static final Logger log = LogManager.getLogger(TokenDecorationRenderer.class); + private final RenderHelper renderHelper; + private final Zone zone; + private final FacingArrowRenderer FACING_ARROW_RENDERER; + private final HaloRenderer HALO_RENDERER; + private final StateRenderer OVERLAY_RENDERER; + + public TokenDecorationRenderer(RenderHelper renderHelper, Zone zone) { + this.renderHelper = renderHelper; + this.zone = zone; + FACING_ARROW_RENDERER = new FacingArrowRenderer(renderHelper, zone); + HALO_RENDERER = new HaloRenderer(renderHelper, zone); + OVERLAY_RENDERER = new StateRenderer(renderHelper, zone); + } + + public void renderDecorations( + boolean under, + ZoneViewModel viewModel, + ZoneViewModel.TokenPosition position, + Graphics2D g2d, + boolean selected, + boolean moving, + boolean hover) { + var timer = CodeTimer.get(); + Composite oldComposite = g2d.getComposite(); + if (hover || selected && !moving) { + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); + } else { + g2d.setComposite( + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, position.token().getTokenOpacity())); + } + timer.increment("TokenDecorationRenderer-render"); + if (under) { + timer.start("TokenDecorationRenderer-renderUnder"); + // paint Halo + renderHelper.render(g2d, worldG -> HALO_RENDERER.renderHalo(worldG, position)); + // paint TOKEN + timer.stop("TokenDecorationRenderer-renderUnder"); + } else { + timer.start("TokenDecorationRenderer-renderOver"); + if (!moving) { + // paint STATE + OVERLAY_RENDERER.renderStates(viewModel, position, g2d, selected, hover); + // paint BAR + OVERLAY_RENDERER.renderBars(viewModel, position, g2d, selected, hover); + } + // paint FACING + FACING_ARROW_RENDERER.paintArrow(g2d, position); + // paint LABEL + // Not yet implemented; + timer.stop("TokenDecorationRenderer-renderOver"); + } + g2d.setComposite(oldComposite); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java index c0c6a0f42d..6282b5658e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java @@ -25,6 +25,7 @@ import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ui.zone.ZoneViewModel; import net.rptools.maptool.client.ui.zone.ZoneViewModel.TokenPosition; import net.rptools.maptool.client.ui.zone.renderer.RenderHelper; import net.rptools.maptool.model.*; @@ -41,17 +42,35 @@ public class TokenRenderer { private final RenderHelper renderHelper; private final Zone zone; + private final TokenDecorationRenderer decorationRenderer; public TokenRenderer(RenderHelper renderHelper, Zone zone) { this.renderHelper = renderHelper; this.zone = zone; + this.decorationRenderer = new TokenDecorationRenderer(renderHelper, zone); } - public void renderToken(Token token, TokenPosition position, Graphics2D g2d, float opacity) { + public void renderToken( + Token token, + ZoneViewModel viewModel, + TokenPosition position, + Graphics2D g2d, + boolean isSelected, + boolean isMoving, + boolean isHover) { var timer = CodeTimer.get(); + decorationRenderer.renderDecorations( + true, viewModel, position, g2d, isSelected, isMoving, isHover); + timer.increment("TokenRenderer-renderToken"); timer.start("TokenRenderer-renderToken"); + // Calculate alpha Transparency from token and use opacity to indicate that token is moving + float opacity = + viewModel.isTokenMoving(token.getId()) + ? Math.max(0.5f, token.getTokenOpacity() / 2f) + : isSelected || isHover ? 1 : token.getTokenOpacity(); + timer.start("TokenRenderer-loadImageTable"); if (token.getHasImageTable() && !imageTableMap.containsKey(token.getImageTableName())) { (new CacheTableImagesWorker(token.getImageTableName())).execute(); @@ -59,9 +78,13 @@ public void renderToken(Token token, TokenPosition position, Graphics2D g2d, flo timer.stop("TokenRenderer-loadImageTable"); timer.start("TokenRenderer-paintTokenImage"); - renderHelper.render( - g2d, worldG -> paintTokenImage(worldG, position, opacity * token.getTokenOpacity())); + + renderHelper.render(g2d, worldG -> paintTokenImage(worldG, position, opacity)); timer.stop("TokenRenderer-paintTokenImage"); + + decorationRenderer.renderDecorations( + false, viewModel, position, g2d, isSelected, isMoving, isHover); + timer.stop("TokenRenderer-renderToken"); } diff --git a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java index 5a80cf7e42..7b085ee1ba 100644 --- a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java +++ b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java @@ -408,7 +408,7 @@ public static Area createLine(int width, Point2D... points) { return new Area(path); } - private static Point2D getPointAtVector(Point2D point, double angle, double length) { + public static Point2D getPointAtVector(Point2D point, double angle, double length) { double x = point.getX() + length * Math.cos(Math.toRadians(angle)); double y = point.getY() - length * Math.sin(Math.toRadians(angle)); return new Point2D.Double(x, y);