From 8cc70440487befa74b436be173addfd23a3f5792 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 29 Jul 2023 01:46:44 -0700 Subject: [PATCH 1/5] Remove noisy hover listener output --- .../maptool/client/ui/sheet/stats/StatSheetListener.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java index 10bb3ab58d..766a7605cc 100644 --- a/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java +++ b/src/main/java/net/rptools/maptool/client/ui/sheet/stats/StatSheetListener.java @@ -39,7 +39,6 @@ public class StatSheetListener { */ @Subscribe public void onHoverEnter(TokenHoverEnter event) { - System.out.println("TokenHoverListener.onHoverEnter"); if (AppPreferences.getShowStatSheet() && AppPreferences.getShowStatSheetModifier() == event.shiftDown()) { var ssManager = new StatSheetManager(); @@ -69,7 +68,6 @@ public void onHoverEnter(TokenHoverEnter event) { */ @Subscribe public void onHoverExit(TokenHoverExit event) { - System.out.println("TokenHoverListener.onHoverLeave"); MapTool.getFrame().showControlPanel(); if (statSheet != null) { statSheet.clearContent(); From 8e6dbc680f91b29688c420928684952db60d36d2 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 29 Jul 2023 01:54:49 -0700 Subject: [PATCH 2/5] Be clear about visible area nullability `ZoneView.getVisibleArea(PlayerView)` is guaranteed to return a non-`null` result. This method is now marked as `@Nonnull` to make this clear, and we can avoid several `null` checks in `ZoneRenderer` as a result. --- .../maptool/client/ui/zone/ZoneRenderer.java | 14 ++++++++------ .../rptools/maptool/client/ui/zone/ZoneView.java | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index 9b10ae0deb..cb399c54c9 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -34,6 +34,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.imageio.ImageIO; import javax.swing.*; @@ -1109,7 +1110,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) { timer.stop("ZoneRenderer-getVisibleArea"); timer.start("createTransformedArea"); - if (a != null && !a.isEmpty()) { + if (!a.isEmpty()) { visibleScreenArea = a.createTransformedArea(af); } timer.stop("createTransformedArea"); @@ -1120,7 +1121,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) { { // renderMoveSelectionSet() requires exposedFogArea to be properly set exposedFogArea = new Area(zone.getExposedArea()); - if (exposedFogArea != null && zone.hasFog()) { + if (zone.hasFog()) { if (visibleScreenArea != null && !visibleScreenArea.isEmpty()) { exposedFogArea.intersect(visibleScreenArea); } else { @@ -1864,10 +1865,10 @@ private void renderFog(Graphics2D g, PlayerView view) { } private void renderFogArea( - final Graphics2D buffG, final PlayerView view, Area softFog, Area visibleArea) { + final Graphics2D buffG, final PlayerView view, Area softFog, @Nonnull Area visibleArea) { if (zoneView.isUsingVision()) { buffG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - if (visibleArea != null && !visibleArea.isEmpty()) { + if (!visibleArea.isEmpty()) { buffG.setColor(new Color(0, 0, 0, AppPreferences.getFogOverlayOpacity())); // Fill in the exposed area @@ -1889,9 +1890,10 @@ private void renderFogArea( } } - private void renderFogOutline(final Graphics2D buffG, PlayerView view, Area visibleArea) { + private void renderFogOutline( + final Graphics2D buffG, PlayerView view, @Nonnull Area visibleArea) { // If there is no visible area, there is no outline that needs rendering. - if (zoneView.isUsingVision() && visibleArea != null && !visibleArea.isEmpty()) { + if (zoneView.isUsingVision() && !visibleArea.isEmpty()) { // Transform the area (not G2D) because we want the drawn line to remain thin. AffineTransform af = new AffineTransform(); af.translate(zoneScale.getOffsetX(), zoneScale.getOffsetY()); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 3fbd165d65..4951b03d3c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -189,7 +189,7 @@ public Area getExposedArea(PlayerView view) { * @param view the PlayerView * @return the visible area */ - public Area getVisibleArea(PlayerView view) { + public @Nonnull Area getVisibleArea(PlayerView view) { return visibleAreaMap.computeIfAbsent( view, view2 -> { From 68e7551e2b3242468100f37d1a65a15b3a1b8515 Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 29 Jul 2023 13:24:17 -0700 Subject: [PATCH 3/5] Cache total darkened areas inside Illuminations We already cached total lit areas, and this change also adds darkness. Just as for lit areas, the necessary computation is done lazily. The terminology has also been cleaned up: the lit area used to be termed "visible area", but `Illumination` doesn't actually know anything about visibility. So `Illumination.getVisibleArea()` has been renamed to `Illumination.getLitArea()`, and other uses of the term "visible" have also been removed. --- .../maptool/client/ui/zone/Illumination.java | 44 +++++++++++++++---- .../client/ui/zone/IlluminationModel.java | 2 +- .../maptool/client/ui/zone/ZoneView.java | 8 ++-- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java b/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java index ed84dda25b..f8a780deb1 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java @@ -38,7 +38,7 @@ *
  • The obscured lumens levels. For each light area in the basic structure, subtract out any * stronger darknesses, and for each darkness subtract out any stronger lights. The result is * the obscured lit areas arranged by lumens level. - *
  • The complete visible area. This is the union of the light areas after the process in (1). + *
  • The complete lit area. This is the union of the light areas after the process in (1). *
  • The disjoint obscured lumens levels. Starting from (1), we can additionally subtract strong * light from weak light and strong darkness from weak darkness so that any given point is * represented only in the strongest lumens level. @@ -105,7 +105,15 @@ public LumensLevel copy() { *

    This is derived from {@link #obscuredLumensLevels} by unioning all light areas and leaving * out all darkness areas. */ - private Area visibleArea = null; + private Area litArea = null; + + /** + * The complete darkened area. + * + *

    This is derived from {@link #obscuredLumensLevels} by unioning all darkness areas and + * leaving out all light areas. + */ + private Area darkenedArea = null; // endregion @@ -113,8 +121,8 @@ public LumensLevel copy() { * Create a new {@code Illumination} from a set of base lumens levels. * *

    The {@code lumensLevels} should contain the complete areas that could be covered - * covered by each level of lumens. Obscurement (darkness competing with light) should not already - * be calculated, as the {@code Illumination} will handle this. + * by each level of lumens. Obscurement (darkness competing with light) should not already be + * calculated, as the {@code Illumination} will handle this. * * @param lumensLevels The base areas covered by each level of lumens. */ @@ -210,18 +218,36 @@ public Optional getObscuredLumensLevel(int lumensStrength) { * Get the total lit area from all lumens levels. * *

    After subtracting stronger darkness from weaker lights, the resulting lights are unioned - * into a single visible area. + * into a single area. * * @return The lit area. */ - public @Nonnull Area getVisibleArea() { - if (visibleArea == null) { + public @Nonnull Area getLitArea() { + if (litArea == null) { final var result = new Area(); getObscuredLumensLevels().forEach(level -> result.add(level.lightArea())); - visibleArea = result; + litArea = result; + } + + return new Area(litArea); + } + + /** + * Get the total dark area from all lumens levels. + * + *

    After subtracting stronger lights from weaker darkness, the resulting darknesses are unioned + * into a single area. + * + * @return The darkened area. + */ + public @Nonnull Area getDarkenedArea() { + if (darkenedArea == null) { + final var result = new Area(); + getObscuredLumensLevels().forEach(level -> result.add(level.darknessArea())); + darkenedArea = result; } - return new Area(visibleArea); + return new Area(darkenedArea); } /** diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java b/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java index a1c349f621..b84c58d76a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java @@ -27,7 +27,7 @@ import net.rptools.maptool.model.LightSource; /** - * Manages the light sources and illuminations of a zone, for a given set of illumniator parameters. + * Manages the light sources and illuminations of a zone, for a given set of illuminator parameters. * *

    This needs to be kept in sync with the associated {@code Zone} in order for the results to * make sense diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 4951b03d3c..5df735f150 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -625,14 +625,14 @@ public Area getVisibleArea(Token token, PlayerView view) { // perspective. final var singleTokenView = new PlayerView(view.getRole(), Collections.singletonList(token)); final var illumination = getIllumination(singleTokenView); - final var visibleArea = illumination.getVisibleArea(); - visibleArea.intersect(tokenVisibleArea); + final var litArea = illumination.getLitArea(); + litArea.intersect(tokenVisibleArea); - tokenVisionCache.put(token.getId(), visibleArea); + tokenVisionCache.put(token.getId(), litArea); // log.info("getVisibleArea: \t\t" + stopwatch); - return visibleArea; + return litArea; } /** From 39d4a80ec4dcbff3e1e81d13e8676c69e49d0bea Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 29 Jul 2023 02:10:37 -0700 Subject: [PATCH 4/5] Separate darkness rendering from light rendering for non-GM players Non-GM players need to have darkness rendered as blackness rather than as lights. We used to accomplish this by rendering each darkness as a regular light (as the GM would see it), then would paint each darkness as black afterwards. Now we don't even represent the darkness as a `DrawableLight` unless the player is a GM. This way we aren't wasting time compositing darkness lights that won't even be visible in the end. We also don't render the darknesses individually, but instead render the entire darkened area as black in a single pass. --- .../maptool/client/ui/zone/ZoneRenderer.java | 53 ++++++++++++++----- .../maptool/client/ui/zone/ZoneView.java | 10 +++- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index cb399c54c9..670f390549 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -1196,6 +1196,8 @@ public void renderZone(Graphics2D g2d, PlayerView view) { timer.stop("auras"); } + renderPlayerDarkness(g2d, view); + /** * The following sections used to handle rendering of the Hidden (i.e. "GM") layer followed by * the Token layer. The problem was that we want all drawables to appear below all tokens, and @@ -1430,20 +1432,6 @@ private void renderLights(Graphics2D g, PlayerView view) { timer.stop("renderLights:renderLightOverlay"); } - if (!view.isGMView()) { - // Note that the ZoneView has already restricted the darkness to its affected areas. - final var darknessLights = - drawableLights.stream().filter(light -> light.getLumens() <= 0).toList(); - renderLightOverlay( - g, - new SolidColorComposite(0xff000000), - AlphaComposite.SrcOver, - LightOverlayClipStyle.CLIP_TO_NOT_VISIBLE_AREA, - darknessLights, - Color.black, - new Color(0, 0, 0, 0)); - } - if (AppState.isShowLumensOverlay()) { // Lumens overlay enabled. timer.start("renderLights:renderLumensOverlay"); @@ -1660,6 +1648,43 @@ private void renderLightOverlay( } } + /** + * Draws a solid black overlay wherever a non-GM player should see darkness. + * + *

    If {@code view} is a GM view, this renders nothing. + * + * @param g The graphics object used to render the zone. + * @param view The player view. + */ + private void renderPlayerDarkness(Graphics2D g, PlayerView view) { + if (view.isGMView()) { + // GMs see the darkness rendered as lights, not as blackness. + return; + } + + final var darkness = zoneView.getIllumination(view).getDarkenedArea(); + if (darkness.isEmpty()) { + // Skip the rendering work if it isn't necessary. + return; + } + + g = (Graphics2D) g.create(); + try { + timer.start("renderPlayerDarkness:setTransform"); + AffineTransform af = new AffineTransform(); + af.translate(getViewOffsetX(), getViewOffsetY()); + af.scale(getScale(), getScale()); + g.setTransform(af); + timer.stop("renderPlayerDarkness:setTransform"); + + g.setComposite(AlphaComposite.Src); + g.setPaint(Color.black); + g.fill(darkness); + } finally { + g.dispose(); + } + } + /** * This outlines the area visible to the token under the cursor, clipped to the current * fog-of-war. This is appropriate for the player view, but the GM sees everything. diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 5df735f150..d0d0775d9a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -520,7 +520,7 @@ private Illumination getIllumination(IlluminationKey illuminationKey) { return personalLights; } - private Illumination getIllumination(PlayerView view) { + public Illumination getIllumination(PlayerView view) { var illumination = illuminationsPerView.get(view); if (illumination == null) { // Not yet calculated. Do so now. @@ -750,6 +750,12 @@ public Collection getDrawableLights(PlayerView view) { .filter(laud -> laud.lightInfo() != null) .map( (ContributedLight laud) -> { + var isDarkness = laud.litArea().lumens() < 0; + if (isDarkness && !view.isGMView()) { + // Non-GM players do not render the light aspect of darkness. + return null; + } + // Make sure each drawable light is restricted to the area it covers, // accounting for darkness effects. final var obscuredArea = new Area(laud.litArea().area()); @@ -761,7 +767,7 @@ public Collection getDrawableLights(PlayerView view) { } obscuredArea.intersect( - laud.litArea().lumens() < 0 + isDarkness ? lumensLevel.get().darknessArea() : lumensLevel.get().lightArea()); return new DrawableLight( From d488009a1f994f0db5af7294bbeec4ba79bbc07b Mon Sep 17 00:00:00 2001 From: Kenneth VanderLinde Date: Sat, 29 Jul 2023 02:08:55 -0700 Subject: [PATCH 5/5] Do not render clear lights Clear lights no longer get mapped to `DrawableLight` instances, making it so that they no longer get rendered. With this change, `DrawableLight`s can no longer have `null` paints. This means we no longer need to support "default paints" in `ZoneRenderer.renderLightOverlay()`, so that parameter and related logic have been removed. --- .../rptools/maptool/client/ui/zone/DrawableLight.java | 11 ++++++----- .../rptools/maptool/client/ui/zone/ZoneRenderer.java | 7 +------ .../net/rptools/maptool/client/ui/zone/ZoneView.java | 11 +++++++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/DrawableLight.java b/src/main/java/net/rptools/maptool/client/ui/zone/DrawableLight.java index 3125c14809..59e44df492 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/DrawableLight.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/DrawableLight.java @@ -15,26 +15,27 @@ package net.rptools.maptool.client.ui.zone; import java.awt.geom.Area; +import javax.annotation.Nonnull; import net.rptools.maptool.model.drawing.DrawablePaint; public class DrawableLight { - private DrawablePaint paint; - private Area area; + private @Nonnull DrawablePaint paint; + private @Nonnull Area area; private int lumens; - public DrawableLight(DrawablePaint paint, Area area, int lumens) { + public DrawableLight(@Nonnull DrawablePaint paint, @Nonnull Area area, int lumens) { super(); this.paint = paint; this.area = area; this.lumens = lumens; } - public DrawablePaint getPaint() { + public @Nonnull DrawablePaint getPaint() { return paint; } - public Area getArea() { + public @Nonnull Area getArea() { return area; } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index 670f390549..bc3691e25c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -1427,7 +1427,6 @@ private void renderLights(Graphics2D g, PlayerView view) { overlayBlending, view.isGMView() ? null : LightOverlayClipStyle.CLIP_TO_VISIBLE_AREA, drawableLights, - Color.black, overlayFillColor); timer.stop("renderLights:renderLightOverlay"); } @@ -1463,7 +1462,6 @@ private void renderAuras(Graphics2D g, PlayerView view) { AlphaComposite.SrcOver, view.isGMView() ? null : LightOverlayClipStyle.CLIP_TO_VISIBLE_AREA, drawableAuras, - new Color(255, 255, 255, 150), new Color(0, 0, 0, 0)); timer.stop("renderAuras:renderAuraOverlay"); } @@ -1580,7 +1578,6 @@ private void renderLumensOverlay( * @param clipStyle How to clip the overlay relative to the visible area. Set to null for no extra * clipping. * @param lights The lights that will be rendered and blended. - * @param defaultPaint A default paint for lights without a paint. */ private void renderLightOverlay( Graphics2D g, @@ -1588,7 +1585,6 @@ private void renderLightOverlay( Composite overlayBlending, @Nullable LightOverlayClipStyle clipStyle, Collection lights, - Paint defaultPaint, Paint backgroundFill) { if (lights.isEmpty()) { // No point spending resources accomplishing nothing. @@ -1631,8 +1627,7 @@ private void renderLightOverlay( // Draw lights onto the buffer image so the map doesn't affect how they blend timer.start("renderLightOverlay:drawLights"); for (var light : lights) { - var paint = light.getPaint() != null ? light.getPaint().getPaint() : defaultPaint; - newG.setPaint(paint); + newG.setPaint(light.getPaint().getPaint()); timer.start("renderLightOverlay:fillLight"); newG.fill(light.getArea()); timer.stop("renderLightOverlay:fillLight"); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index d0d0775d9a..2c84f3e29c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -756,6 +756,12 @@ public Collection getDrawableLights(PlayerView view) { return null; } + // Lights without a colour are "clear" and should not be rendered. + var paint = laud.lightInfo().light().getPaint(); + if (paint == null) { + return null; + } + // Make sure each drawable light is restricted to the area it covers, // accounting for darkness effects. final var obscuredArea = new Area(laud.litArea().area()); @@ -770,10 +776,7 @@ public Collection getDrawableLights(PlayerView view) { isDarkness ? lumensLevel.get().darknessArea() : lumensLevel.get().lightArea()); - return new DrawableLight( - laud.lightInfo().light().getPaint(), - obscuredArea, - laud.litArea().lumens()); + return new DrawableLight(paint, obscuredArea, laud.litArea().lumens()); }) .filter(Objects::nonNull) .toList();