From bb42cc4214bfb58e18a4b76d7c48e5fcdb50be9c Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:58:09 +0800 Subject: [PATCH 01/24] Token class: clean up some property names, e.g. isFlippedIso to flippedIso so that getter becomes isFlippedIso(). Unnecessary Token removed from property tokenOpacity. Now just opacity. Reordered to keep layout props together. Added new layout prop imageRotation Updated tokenLayoutProp macro functions --- .../maptool/client/functions/TokenImage.java | 4 +- .../functions/TokenPropertyFunctions.java | 84 ++++- .../client/ui/zone/renderer/ZoneRenderer.java | 6 +- .../java/net/rptools/maptool/model/Token.java | 324 +++++++++--------- 4 files changed, 250 insertions(+), 168 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenImage.java b/src/main/java/net/rptools/maptool/client/functions/TokenImage.java index f9fe2186d2..0ed182c66f 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenImage.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenImage.java @@ -111,7 +111,7 @@ public Object childEvaluate( token = FunctionUtil.getTokenFromParam(resolver, functionName, args, 1, 2); MapTool.serverCommand().updateTokenProperty(token, Token.Update.setTokenOpacity, strOpacity); - return token.getTokenOpacity(); + return token.getOpacity(); } if (functionName.equalsIgnoreCase("getTokenOpacity")) { @@ -121,7 +121,7 @@ public Object childEvaluate( FunctionUtil.checkNumberParam(functionName, args, 0, 2); token = FunctionUtil.getTokenFromParam(resolver, functionName, args, 0, 1); - return token.getTokenOpacity(); + return token.getOpacity(); } if (functionName.equalsIgnoreCase("setTokenImage")) { diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java index 23ac082b60..8e1fbd9150 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java @@ -114,6 +114,7 @@ private TokenPropertyFunctions() { "setNotes", "getTokenLayoutProps", "setTokenLayoutProps", + "setExtendedTokenLayoutProps", "setTokenSnapToGrid", "getAllowsURIAccess", "setAllowsURIAccess", @@ -712,7 +713,7 @@ public Object childEvaluate( if (functionName.equalsIgnoreCase("isFlippedIso")) { FunctionUtil.checkNumberParam(functionName, parameters, 0, 2); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 0, 1); - return token.isFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; + return token.getIsFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; } /* @@ -921,7 +922,7 @@ public Object childEvaluate( FunctionUtil.checkNumberParam(functionName, parameters, 0, 2); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 0, 1); MapTool.serverCommand().updateTokenProperty(token, Token.Update.flipIso); - return token.isFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; + return token.getIsFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; } /* @@ -929,24 +930,91 @@ public Object childEvaluate( */ if (functionName.equalsIgnoreCase("getTokenLayoutProps")) { FunctionUtil.checkNumberParam(functionName, parameters, 0, 3); - String delim = parameters.size() > 0 ? parameters.get(0).toString() : ","; + String delim = parameters.size() > 0 ? parameters.get(0).toString() : ";"; Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 1, 2); - Double scale = token.getSizeScale(); - int xOffset = token.getAnchorX(); - int yOffset = token.getAnchorY(); + String scale = String.valueOf(token.getSizeScale()); + String xOffset = String.valueOf(token.getAnchorX()); + String yOffset = String.valueOf(token.getAnchorY()); + String scaleX = String.valueOf(token.getScaleX()); + String scaleY = String.valueOf(token.getScaleY()); + String footprintScaleValue = + String.valueOf( + token.getFootprint(token.getZoneRenderer().getZone().getGrid()).getScale()); if ("json".equals(delim)) { JsonObject jarr = new JsonObject(); jarr.addProperty("scale", scale); jarr.addProperty("xOffset", xOffset); jarr.addProperty("yOffset", yOffset); + jarr.addProperty("scaleX", scaleX); + jarr.addProperty("scaleY", scaleY); + jarr.addProperty("footprintScale", footprintScaleValue); return jarr; } else { - return "scale=" + scale + delim + "xOffset=" + xOffset + delim + "yOffset=" + yOffset; + return "scale=" + + scale + + delim + + "xOffset=" + + xOffset + + delim + + "yOffset=" + + yOffset + + delim + + "scaleX=" + + scaleX + + delim + + "scaleY=" + + scaleY + + delim + + "footprintScale=" + + delim + + footprintScaleValue; + } + } + /* + * setExtendedTokenLayoutProps(StrProp/JSON Object, token: currentToken(), mapName = current map) + */ + if (functionName.equalsIgnoreCase("setExtendedTokenLayoutProps")) { + FunctionUtil.checkNumberParam(functionName, parameters, 1, 3); + Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 1, 2); + JsonObject json; + try { // try for json object + json = FunctionUtil.paramAsJsonObject("setExtendedTokenLayoutProps", parameters, 0); + } catch (ParserException pe) { + try { // try for strProp + json = + JSONMacroFunctions.getInstance() + .getJsonObjectFunctions() + .fromStrProp( + FunctionUtil.paramAsString( + "setExtendedTokenLayoutProps", parameters, 0, true), + ";"); + } catch (ParserException pe2) { + throw new ParserException( + I18N.getText( + "macro.function.input.illegalArgumentType", "unknown", "JSON Object/StrProp")); + } + } + if (json != null && json.isJsonObject()) { + JsonObject jobj = json.getAsJsonObject(); + for (String s : jobj.keySet()) { + switch (s.toLowerCase()) { + case "scale" -> token.setSizeScale(jobj.get(s).getAsDouble()); + case "xoffset" -> token.setAnchor(jobj.get(s).getAsInt(), token.getAnchorY()); + case "yoffset" -> token.setAnchor(token.getAnchorX(), jobj.get(s).getAsInt()); + case "scalex" -> token.setScaleX(jobj.get(s).getAsDouble()); + case "scaley" -> token.setScaleY(jobj.get(s).getAsDouble()); + default -> { + continue; + } + } + } + return true; + } else { + return false; } } - /* * setTokenLayoutProps(scale, xOffset, yOffset, token: currentToken(), mapName = current map) */ 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 380057c1e7..52a365d108 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 @@ -1444,7 +1444,7 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set wig.dispose(); } // on the iso plane - if (token.isFlippedIso()) { + if (token.getIsFlippedIso()) { if (flipIsoImageMap.get(token) == null) { workImage = IsometricGrid.isoImage(workImage); } else { @@ -2330,7 +2330,7 @@ protected void renderTokens( timer.stop("tokenlist-5"); timer.start("tokenlist-5a"); - if (token.isFlippedIso()) { + if (token.getIsFlippedIso()) { if (flipIsoImageMap.get(token) == null) { workImage = IsometricGrid.isoImage(workImage); flipIsoImageMap.put(token, workImage); @@ -2414,7 +2414,7 @@ protected void renderTokens( haloRenderer.renderHalo(tokenG, token, location); // Calculate alpha Transparency from token and use opacity for indicating that token is moving - float opacity = token.getTokenOpacity(); + float opacity = token.getOpacity(); if (isTokenMoving(token)) opacity = opacity / 2.0f; // Finally render the token image diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 530610473a..a27c3787b2 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -94,6 +94,8 @@ public class Token implements Cloneable { public static final String NUM_ON_BOTH = "Both"; public static final String LIB_TOKEN_PREFIX = "lib:"; + private static final int OWNER_TYPE_ALL = 1; + private static final int OWNER_TYPE_LIST = 0; private boolean beingImpersonated = false; private GUID exposedAreaGUID = new GUID(); @@ -187,6 +189,7 @@ public enum Update { setScaleX, setScaleY, setScaleXY, + setImageRotation, setNotes, setGMNotes, saveMacro, @@ -232,32 +235,30 @@ public enum Update { private int x; private int y; private int z; + private Integer facing = null; + private int lastX; + private int lastY; + private Path lastPath; + // Token Layout private int anchorX; private int anchorY; - private double sizeScale = 1; + private double scaleX = 1; + private double scaleY = 1; + private double imageRotation = 0; + private boolean snapToScale = true; // Is size based on footprint - private int lastX; - private int lastY; - private Path lastPath; - - private boolean snapToScale = true; // Whether the scaleX and scaleY represent snap-to-grid // measurements - // These are the original image width and height private int width; private int height; private int isoWidth; private int isoHeight; - private double scaleX = 1; - private double scaleY = 1; - private Map sizeMap = new HashMap<>(); - private boolean snapToGrid = true; // Whether the token snaps to the current grid or is free - // floating + private boolean snapToGrid = true; // Is token position constrained to grid increments private boolean isVisible = true; private boolean visibleOnlyToOwner = false; @@ -266,24 +267,17 @@ public enum Update { // before token is shown over FoW private boolean isAlwaysVisible = false; // Controls whether a Token is shown over VBL - // region Topology masks - + // Topology masks private Area vbl; private Area hillVbl; private Area pitVbl; private Area coverVbl; private Area mbl; - // endregion - private String name = ""; private Set ownerList = new HashSet<>(); - private int ownerType; - private static final int OWNER_TYPE_ALL = 1; - private static final int OWNER_TYPE_LIST = 0; - private String tokenShape = TokenShape.SQUARE.toString(); private String tokenType = Type.NPC.toString(); private String layer = Zone.Layer.getDefaultPlayerLayer().toString(); @@ -292,8 +286,6 @@ public enum Update { private String propertyType = MapTool.getCampaign().getCampaignProperties().getDefaultTokenPropertyType(); - private Integer facing = null; - private Integer haloColorValue; private transient Color haloColor; @@ -301,7 +293,7 @@ public enum Update { private transient Color visionOverlayColor; // Jamz: allow token alpha channel modification - private @Nonnull Float tokenOpacity = 1.0f; + private @Nonnull Float opacity = 1.0f; private String speechName = ""; @@ -427,9 +419,6 @@ public Token(Token token) { height = token.height; isoWidth = token.isoWidth; isoHeight = token.isoHeight; - scaleX = token.scaleX; - scaleY = token.scaleY; - facing = token.facing; tokenShape = token.tokenShape; tokenType = token.tokenType; haloColorValue = token.haloColorValue; @@ -455,9 +444,16 @@ public Token(Token token) { gmNotesType = token.gmNotesType; label = token.label; + anchorX = token.anchorX; + anchorY = token.anchorY; + facing = token.facing; isFlippedX = token.isFlippedX; isFlippedY = token.isFlippedY; isFlippedIso = token.isFlippedIso; + imageRotation = token.imageRotation; + scaleX = token.scaleX; + scaleY = token.scaleY; + sizeScale = token.sizeScale; layer = token.layer; @@ -465,9 +461,6 @@ public Token(Token token) { charsheetImage = token.charsheetImage; portraitImage = token.portraitImage; - anchorX = token.anchorX; - anchorY = token.anchorY; - sizeScale = token.sizeScale; sightType = token.sightType; hasSight = token.hasSight; propertyType = token.propertyType; @@ -508,7 +501,7 @@ public Token(Token token) { exposedAreaGUID = token.exposedAreaGUID; heroLabData = token.heroLabData; - tokenOpacity = token.tokenOpacity; + opacity = token.opacity; terrainModifier = token.terrainModifier; terrainModifierOperation = token.terrainModifierOperation; terrainModifiersIgnored.addAll(token.terrainModifiersIgnored); @@ -594,7 +587,7 @@ public void setImageTableName(String imageTableName) { } public void setWidth(int width) { - if (isFlippedIso()) { + if (getIsFlippedIso()) { isoWidth = width; } else { this.width = width; @@ -602,7 +595,7 @@ public void setWidth(int width) { } public void setHeight(int height) { - if (isFlippedIso()) { + if (getIsFlippedIso()) { isoHeight = height; } else { this.height = height; @@ -610,7 +603,7 @@ public void setHeight(int height) { } public int getWidth() { - if (isFlippedIso() && isoWidth != 0) { + if (getIsFlippedIso() && isoWidth != 0) { return isoWidth; } else { return width; @@ -618,7 +611,7 @@ public int getWidth() { } public int getHeight() { - if (isFlippedIso() && isoHeight != 0) { + if (getIsFlippedIso() && isoHeight != 0) { return isoHeight; } else { return height; @@ -701,8 +694,8 @@ public Color getHaloColor() { /** * @return The token opacity, in the range [0.0f, 1.0f]. */ - public float getTokenOpacity() { - return tokenOpacity; + public float getOpacity() { + return opacity; } /** @@ -710,7 +703,7 @@ public float getTokenOpacity() { * * @param alpha the float of the opacity. */ - public void setTokenOpacity(float alpha) { + public void setOpacity(float alpha) { if (alpha > 1.0f) { alpha = 1.0f; } @@ -718,7 +711,7 @@ public void setTokenOpacity(float alpha) { alpha = 0.0f; } - tokenOpacity = alpha; + opacity = alpha; } /** @@ -852,47 +845,6 @@ public void setLayer(Zone.Layer layer) { actualLayer = layer; } - public boolean hasFacing() { - return facing != null; - } - - public void setFacing(int facing) { - while (facing > 180 || facing < -179) { - facing += facing > 180 ? -360 : 0; - facing += facing < -179 ? 360 : 0; - } - this.facing = facing; - } - - public void removeFacing() { - this.facing = null; - } - - /** - * Facing is in the map space where 0 degrees is along the X axis to the right and proceeding CCW - * for positive angles. - * - *

Round/Square tokens that have no facing set, return null. Top Down tokens default to -90. - * - * @return null or angle in degrees - */ - public int getFacing() { - // -90° is natural alignment. TODO This should really be a per grid setting - return facing == null ? -90 : facing; - } - - /** - * This returns the rotation of the facing of the token from the default facing of down or -90. - * - *

Positive for CW and negative for CCW. The range is currently from -270° (inclusive) to +90° - * (exclusive), but callers should not rely on this. - * - * @return angle in degrees - */ - public int getFacingInDegrees() { - return -getFacing() - 90; - } - public boolean getHasSight() { return hasSight; } @@ -1299,22 +1251,6 @@ public Path getLastPath() { return lastPath; } - public double getScaleX() { - return scaleX; - } - - public double getScaleY() { - return scaleY; - } - - public void setScaleX(double scaleX) { - this.scaleX = scaleX; - } - - public void setScaleY(double scaleY) { - this.scaleY = scaleY; - } - /** * Returns whether the token is constrained to a pre-defined grid size. * @@ -1535,7 +1471,7 @@ public Area getTransformedMaskTopology(Area areaToTransform) { atArea.concatenate(AffineTransform.getTranslateInstance(0, -getHeight())); } - if (isFlippedIso()) { + if (getIsFlippedIso()) { return new Area(atArea.createTransformedShape(IsometricGrid.isoArea(areaToTransform))); } @@ -2160,33 +2096,6 @@ public void setNotesType(String type) { notesType = type; } - public boolean isFlippedY() { - return isFlippedY; - } - - public void setFlippedY(boolean isFlippedY) { - this.isFlippedY = isFlippedY; - } - - public boolean isFlippedX() { - return isFlippedX; - } - - public void setFlippedX(boolean isFlippedX) { - this.isFlippedX = isFlippedX; - } - - public boolean isFlippedIso() { - if (isFlippedIso != null) { - return isFlippedIso; - } - return false; - } - - public void setFlippedIso(boolean isFlippedIso) { - this.isFlippedIso = isFlippedIso; - } - public Color getVisionOverlayColor() { if (visionOverlayColor == null && visionOverlayColorValue != null) { visionOverlayColor = new Color(visionOverlayColorValue); @@ -2208,6 +2117,7 @@ public String toString() { return "Token: " + id; } + // Token Layout - Getters/Setters public void setAnchor(int x, int y) { anchorX = x; anchorY = y; @@ -2218,27 +2128,127 @@ public Point getAnchor() { } public int getAnchorX() { - return anchorX; + return this.anchorX; + } + + public void setAnchorX(int xAnchor) { + this.anchorX = xAnchor; } public int getAnchorY() { - return anchorY; + return this.anchorY; + } + + public void setAnchorY(int yAnchor) { + this.anchorY = yAnchor; + } + + public boolean getIsFlippedIso() { + if (isFlippedIso != null) { + return isFlippedIso; + } + return false; + } + + public void setIsFlippedIso(boolean isFlippedIso) { + this.isFlippedIso = isFlippedIso; + } + + public boolean isFlippedX() { + return isFlippedX; + } + + public void setFlippedX(boolean flippedX) { + this.isFlippedX = flippedX; + } + + public boolean isFlippedY() { + return isFlippedY; + } + + public void setFlippedY(boolean flippedY) { + this.isFlippedY = flippedY; + } + + public double getImageRotation() { + return this.imageRotation; + } + + public void setImageRotation(double angle) { + this.imageRotation = angle; + } + + public double getScaleX() { + return this.scaleX; + } + + public void setScaleX(double scaleX) { + this.scaleX = scaleX; + } + + public double getScaleY() { + return this.scaleY; + } + + public void setScaleY(double scaleY) { + this.scaleY = scaleY; } /** - * @return the scale of the token layout + * @return the scale of the image in token layout */ public double getSizeScale() { - return sizeScale; + return this.sizeScale; } /** - * Set the scale of the token layout + * Set the scale of the image in token layout * * @param scale the scale of the token */ public void setSizeScale(double scale) { - sizeScale = scale; + this.sizeScale = scale; + } + + public boolean hasFacing() { + return facing != null; + } + + public void setFacing(int direction) { + while (direction > 180 || direction < -179) { + direction += direction > 180 ? -360 : 0; + direction += direction < -179 ? 360 : 0; + } + this.facing = direction; + } + + public void removeFacing() { + this.facing = null; + } + + /** + * Facing is in the map space where 0 degrees is along the X axis to the right and proceeding CCW + * for positive angles. + * + *

Round/Square tokens that have no facing set, return null. Top Down tokens default to -90. + * + * @return null or angle in degrees + */ + public int getFacing() { + // -90° is natural alignment. TODO This should really be a per grid setting + return facing == null ? -90 : facing; + } + + /** + * This returns the rotation of the facing of the token from the default facing of down or -90. + * + *

Positive for CW and negative for CCW. The range is currently from -270° (inclusive) to +90° + * (exclusive), but callers should not rely on this. + * + * @return angle in degrees + */ + public int getFacingInDegrees() { + return -getFacing() - 90; } /** @@ -2260,8 +2270,8 @@ public TokenTransferData toTransferData() { td.put(TokenTransferData.ASSET_ID, imageAssetMap.get(null)); td.put(TokenTransferData.Z, z); td.put(TokenTransferData.SNAP_TO_SCALE, snapToScale); - td.put(TokenTransferData.WIDTH, scaleX); - td.put(TokenTransferData.HEIGHT, scaleY); + td.put(TokenTransferData.WIDTH, scaleX); // this makes no sense + td.put(TokenTransferData.HEIGHT, scaleY); // ditto td.put(TokenTransferData.SNAP_TO_GRID, snapToGrid); td.put(TokenTransferData.OWNER_TYPE, ownerType); td.put(TokenTransferData.VISIBLE_OWNER_ONLY, visibleOnlyToOwner); @@ -2623,10 +2633,10 @@ protected Object readResolve() { } // Pre 1.13 - if (tokenOpacity == null) { - tokenOpacity = 1.f; + if (opacity == null) { + opacity = 1.f; } - tokenOpacity = Math.max(0.f, Math.min(tokenOpacity, 1.f)); + opacity = Math.max(0.f, Math.min(opacity, 1.f)); return this; } @@ -2835,7 +2845,7 @@ public void updateProperty(Zone zone, Update update, List setIsAlwaysVisible(parameters.get(0).getBoolValue()); break; case setTokenOpacity: - setTokenOpacity(Float.parseFloat(parameters.get(0).getStringValue())); + setOpacity(Float.parseFloat(parameters.get(0).getStringValue())); break; case setTerrainModifier: setTerrainModifier(parameters.get(0).getDoubleValue()); @@ -2932,7 +2942,7 @@ public void updateProperty(Zone zone, Update update, List setFlippedY(!isFlippedY()); break; case flipIso: - setFlippedIso(!isFlippedIso()); + setIsFlippedIso(!getIsFlippedIso()); break; } if (lightChanged) { @@ -2967,17 +2977,23 @@ public static Token fromDto(TokenDto dto) { token.lastY = dto.getLastY(); token.y = dto.getY(); token.z = dto.getZ(); - token.anchorX = dto.getAnchorX(); - token.anchorY = dto.getAnchorY(); - token.sizeScale = dto.getSizeScale(); token.lastPath = dto.hasLastPath() ? Path.fromDto(dto.getLastPath()) : null; token.snapToScale = dto.getSnapToScale(); token.width = dto.getWidth(); token.height = dto.getHeight(); token.isoWidth = dto.getIsoWidth(); token.isoHeight = dto.getIsoHeight(); + // Layout + token.anchorX = dto.getAnchorX(); + token.anchorY = dto.getAnchorY(); token.scaleX = dto.getScaleX(); token.scaleY = dto.getScaleY(); + token.sizeScale = dto.getSizeScale(); + token.isFlippedX = dto.getFlippedX(); + token.isFlippedY = dto.getFlippedY(); + token.isFlippedIso = dto.getFlippedIso(); + token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null; + dto.getSizeMapMap().forEach((k, v) -> token.sizeMap.put(k, GUID.valueOf(v))); token.snapToGrid = dto.getSnapToGrid(); token.isVisible = dto.getIsVisible(); @@ -2997,22 +3013,19 @@ public static Token fromDto(TokenDto dto) { token.tokenType = dto.getTokenType(); token.layer = dto.getLayer(); token.propertyType = dto.getPropertyType(); - token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null; token.haloColorValue = dto.hasHaloColor() ? dto.getHaloColor().getValue() : null; token.visionOverlayColorValue = dto.hasVisionOverlayColor() ? dto.getVisionOverlayColor().getValue() : null; - token.tokenOpacity = dto.getTokenOpacity(); + token.opacity = dto.getTokenOpacity(); token.speechName = dto.getSpeechName(); token.terrainModifier = dto.getTerrainModifier(); token.terrainModifierOperation = - Token.TerrainModifierOperation.valueOf(dto.getTerrainModifierOperation().name()); + TerrainModifierOperation.valueOf(dto.getTerrainModifierOperation().name()); token.terrainModifiersIgnored.addAll( dto.getTerrainModifiersIgnoredList().stream() - .map(m -> Token.TerrainModifierOperation.valueOf(m.name())) + .map(m -> TerrainModifierOperation.valueOf(m.name())) .collect(Collectors.toList())); - token.isFlippedX = dto.getIsFlippedX(); - token.isFlippedY = dto.getIsFlippedY(); - token.isFlippedIso = dto.getIsFlippedIso(); + token.charsheetImage = dto.hasCharsheetImage() ? new MD5Key(dto.getCharsheetImage().getValue()) : null; token.portraitImage = @@ -3079,9 +3092,14 @@ public TokenDto toDto() { dto.setLastY(lastY); dto.setY(y); dto.setZ(z); + if (facing != null) { + dto.setFacing(Int32Value.of(facing)); + } dto.setAnchorX(anchorX); dto.setAnchorY(anchorY); dto.setSizeScale(sizeScale); + dto.setScaleX(scaleX); + dto.setScaleY(scaleY); if (lastPath != null) { dto.setLastPath(lastPath.toDto()); } @@ -3090,8 +3108,6 @@ public TokenDto toDto() { dto.setHeight(height); dto.setIsoWidth(isoWidth); dto.setIsoHeight(isoHeight); - dto.setScaleX(scaleX); - dto.setScaleY(scaleY); sizeMap.forEach((k, v) -> dto.putSizeMap(k, v.toString())); dto.setSnapToGrid(snapToGrid); dto.setIsVisible(isVisible); @@ -3121,16 +3137,14 @@ public TokenDto toDto() { dto.setTokenType(tokenType); dto.setLayer(layer); dto.setPropertyType(propertyType); - if (facing != null) { - dto.setFacing(Int32Value.of(facing)); - } + if (haloColorValue != null) { dto.setHaloColor(Int32Value.of(haloColorValue)); } if (visionOverlayColorValue != null) { dto.setVisionOverlayColor(Int32Value.of(visionOverlayColorValue)); } - dto.setTokenOpacity(tokenOpacity); + dto.setTokenOpacity(opacity); dto.setSpeechName(speechName); dto.setTerrainModifier(terrainModifier); dto.setTerrainModifierOperation( @@ -3139,9 +3153,9 @@ public TokenDto toDto() { terrainModifiersIgnored.stream() .map(m -> TerrainModifierOperationDto.valueOf(m.name())) .collect(Collectors.toList())); - dto.setIsFlippedX(isFlippedX); - dto.setIsFlippedY(isFlippedY); - dto.setIsFlippedIso(isFlippedIso); + dto.setFlippedX(isFlippedX); + dto.setFlippedY(isFlippedY); + dto.setFlippedIso(isFlippedIso); if (charsheetImage != null) { dto.setCharsheetImage(StringValue.of(charsheetImage.toString())); } From 89c141179999c5ea831314d8dd3ba8b06f946288 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:36:39 +0800 Subject: [PATCH 02/24] Updated TokenDto to handle refactored property names and new property imageRotation --- src/main/proto/data_transfer_objects.proto | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto index 7daec786ec..11fa726195 100644 --- a/src/main/proto/data_transfer_objects.proto +++ b/src/main/proto/data_transfer_objects.proto @@ -305,6 +305,12 @@ message TokenDto { int32 anchor_x = 9; int32 anchor_y = 10; double size_scale = 11; + double scale_x = 20; + double scale_y = 21; + double image_rotation = 73; + bool flipped_x = 45; + bool flipped_y = 46; + bool flipped_iso = 47; int32 last_x = 12; int32 last_y = 13; PathDto last_path = 14; @@ -313,8 +319,6 @@ message TokenDto { int32 height = 17; int32 iso_width = 18; int32 iso_height = 19; - double scale_x = 20; - double scale_y = 21; map size_map = 22; bool snap_to_grid = 23; bool is_visible = 24; @@ -342,9 +346,6 @@ message TokenDto { double terrain_modifier = 42; TerrainModifierOperationDto terrain_modifier_operation = 43; repeated TerrainModifierOperationDto terrain_modifiers_ignored = 44; - bool is_flipped_x = 45; - bool is_flipped_y = 46; - bool is_flipped_iso = 47; google.protobuf.StringValue charsheet_image = 48; google.protobuf.StringValue portrait_image = 49; repeated LightSourceDto unique_light_sources = 72; From 3da8efcb249c8305752a3534f1f9b23db72edd91 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:03:28 +0800 Subject: [PATCH 03/24] Updated Token again due to mysterious update overwriting it --- .../java/net/rptools/maptool/model/Token.java | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index a27c3787b2..4592d4890e 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -323,9 +323,9 @@ public String toString() { private Set terrainModifiersIgnored = new HashSet<>(Collections.singletonList(TerrainModifierOperation.NONE)); - private boolean isFlippedX; - private boolean isFlippedY; - private Boolean isFlippedIso = false; + private boolean flippedX; + private boolean flippedY; + private Boolean flippedIso = false; private MD5Key charsheetImage; private MD5Key portraitImage; @@ -447,9 +447,9 @@ public Token(Token token) { anchorX = token.anchorX; anchorY = token.anchorY; facing = token.facing; - isFlippedX = token.isFlippedX; - isFlippedY = token.isFlippedY; - isFlippedIso = token.isFlippedIso; + flippedX = token.flippedX; + flippedY = token.flippedY; + flippedIso = token.flippedIso; imageRotation = token.imageRotation; scaleX = token.scaleX; scaleY = token.scaleY; @@ -587,7 +587,7 @@ public void setImageTableName(String imageTableName) { } public void setWidth(int width) { - if (getIsFlippedIso()) { + if (isFlippedIso()) { isoWidth = width; } else { this.width = width; @@ -595,7 +595,7 @@ public void setWidth(int width) { } public void setHeight(int height) { - if (getIsFlippedIso()) { + if (isFlippedIso()) { isoHeight = height; } else { this.height = height; @@ -603,7 +603,7 @@ public void setHeight(int height) { } public int getWidth() { - if (getIsFlippedIso() && isoWidth != 0) { + if (isFlippedIso() && isoWidth != 0) { return isoWidth; } else { return width; @@ -611,7 +611,7 @@ public int getWidth() { } public int getHeight() { - if (getIsFlippedIso() && isoHeight != 0) { + if (isFlippedIso() && isoHeight != 0) { return isoHeight; } else { return height; @@ -1461,17 +1461,17 @@ public Area getTransformedMaskTopology(Area areaToTransform) { atArea.concatenate(AffineTransform.getScaleInstance(scalerX, scalerY)); // Lets account for flipped images... - if (isFlippedX) { + if (flippedX) { atArea.concatenate(AffineTransform.getScaleInstance(-1.0, 1.0)); atArea.concatenate(AffineTransform.getTranslateInstance(-getWidth(), 0)); } - if (isFlippedY) { + if (flippedY) { atArea.concatenate(AffineTransform.getScaleInstance(1.0, -1.0)); atArea.concatenate(AffineTransform.getTranslateInstance(0, -getHeight())); } - if (getIsFlippedIso()) { + if (isFlippedIso()) { return new Area(atArea.createTransformedShape(IsometricGrid.isoArea(areaToTransform))); } @@ -2143,31 +2143,31 @@ public void setAnchorY(int yAnchor) { this.anchorY = yAnchor; } - public boolean getIsFlippedIso() { - if (isFlippedIso != null) { - return isFlippedIso; + public boolean isFlippedIso() { + if (flippedIso != null) { + return flippedIso; } return false; } - public void setIsFlippedIso(boolean isFlippedIso) { - this.isFlippedIso = isFlippedIso; + public void setFlippedIso(boolean flippedIso) { + this.flippedIso = flippedIso; } public boolean isFlippedX() { - return isFlippedX; + return flippedX; } public void setFlippedX(boolean flippedX) { - this.isFlippedX = flippedX; + this.flippedX = flippedX; } public boolean isFlippedY() { - return isFlippedY; + return flippedY; } public void setFlippedY(boolean flippedY) { - this.isFlippedY = flippedY; + this.flippedY = flippedY; } public double getImageRotation() { @@ -2593,8 +2593,8 @@ protected Object readResolve() { if (speechMap == null) { speechMap = new HashMap<>(); } - if (isFlippedIso == null) { - isFlippedIso = false; + if (flippedIso == null) { + flippedIso = false; } if (hasImageTable == null) { hasImageTable = false; @@ -2942,7 +2942,7 @@ public void updateProperty(Zone zone, Update update, List setFlippedY(!isFlippedY()); break; case flipIso: - setIsFlippedIso(!getIsFlippedIso()); + setFlippedIso(!isFlippedIso()); break; } if (lightChanged) { @@ -2989,9 +2989,9 @@ public static Token fromDto(TokenDto dto) { token.scaleX = dto.getScaleX(); token.scaleY = dto.getScaleY(); token.sizeScale = dto.getSizeScale(); - token.isFlippedX = dto.getFlippedX(); - token.isFlippedY = dto.getFlippedY(); - token.isFlippedIso = dto.getFlippedIso(); + token.flippedX = dto.getFlippedX(); + token.flippedY = dto.getFlippedY(); + token.flippedIso = dto.getFlippedIso(); token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null; dto.getSizeMapMap().forEach((k, v) -> token.sizeMap.put(k, GUID.valueOf(v))); @@ -3153,9 +3153,9 @@ public TokenDto toDto() { terrainModifiersIgnored.stream() .map(m -> TerrainModifierOperationDto.valueOf(m.name())) .collect(Collectors.toList())); - dto.setFlippedX(isFlippedX); - dto.setFlippedY(isFlippedY); - dto.setFlippedIso(isFlippedIso); + dto.setFlippedX(flippedX); + dto.setFlippedY(flippedY); + dto.setFlippedIso(flippedIso); if (charsheetImage != null) { dto.setCharsheetImage(StringValue.of(charsheetImage.toString())); } From bd46d0646543721ad098a5b8e7af59a0f79f679c Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:03:28 +0800 Subject: [PATCH 04/24] Many things coming together --- src/main/java/net/rptools/lib/MathUtil.java | 150 ++ .../java/net/rptools/lib/image/ImageUtil.java | 666 ++++++--- .../maptool/client/functions/TokenImage.java | 2 +- .../functions/TokenPropertyFunctions.java | 4 +- .../client/swing/SpinnerSliderPaired.java | 567 +++++++ .../client/ui/token/BooleanTokenOverlay.java | 2 +- .../ui/token/dialog/edit/EditTokenDialog.java | 104 +- .../dialog/edit/TokenLayoutPanelHelper.java | 1317 +++++++++++++++++ .../dialog/edit/TokenLayoutRenderPanel.java | 351 +++++ .../dialog/edit/TokenPropertiesDialog.form | 1062 +++++++++---- .../dialog/edit/TokenPropertiesDialog.java | 2 +- .../client/ui/zone/renderer/ZoneRenderer.java | 4 +- .../java/net/rptools/maptool/model/Token.java | 66 +- .../rptools/maptool/util/GraphicsUtil.java | 60 +- src/main/proto/data_transfer_objects.proto | 2 +- .../rptools/maptool/language/i18n.properties | 43 +- 16 files changed, 3813 insertions(+), 589 deletions(-) create mode 100644 src/main/java/net/rptools/lib/MathUtil.java create mode 100644 src/main/java/net/rptools/maptool/client/swing/SpinnerSliderPaired.java create mode 100644 src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java create mode 100644 src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java diff --git a/src/main/java/net/rptools/lib/MathUtil.java b/src/main/java/net/rptools/lib/MathUtil.java new file mode 100644 index 0000000000..08940ab2a3 --- /dev/null +++ b/src/main/java/net/rptools/lib/MathUtil.java @@ -0,0 +1,150 @@ +/* + * 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.lib; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/* Utility class for useful mathematical methods */ +public class MathUtil { + private static final Logger log = LogManager.getLogger(MathUtil.class); + + /** + * Faster version of absolute for integers + * + * @param val + * @return absolute value of val + */ + public static int abs(int val) { + return (val >> 31 ^ val) - (val >> 31); + } + + /** + * Faster version of absolute + * + * @param val + * @return absolute value of val + */ + public static T abs(T val) { + return (T) (val.floatValue() < 0 ? -1 * val.doubleValue() : val); + } + + /** + * Returns a truncated double with the specified number of decimal places + * + * @param value to be truncated + * @param decimalPlaces number of decimal places to use + * @return truncated double value + */ + public static double doublePrecision(double value, int decimalPlaces) { + double d = Double.parseDouble(String.format("%." + decimalPlaces + "f", value)); + log.debug("value: " + value + ", decimalPlaces: " + decimalPlaces + " -> " + d); + return d; + } + + public static boolean isDouble(Object o) { + return o.getClass().isAssignableFrom(Double.class); + } + + public static boolean isFloat(Object o) { + return o.getClass().isAssignableFrom(Float.class); + } + + public static boolean isInt(Object o) { + return o.getClass().isAssignableFrom(Integer.class); + } + + public static boolean isNumber(Object o) { + return o.getClass().isAssignableFrom(Number.class); + } + + /** + * Checks that a value lies within a specified tolerance. Useful for checking if a value is "close + * enough" + * + * @param checkValue to be checked + * @param referenceValue to be checked against + * @param tolerance variance allowed + * @return true if the value is within ± tolerance + */ + public static boolean inTolerance(double checkValue, double referenceValue, double tolerance) { + return checkValue <= referenceValue + tolerance && checkValue >= referenceValue - tolerance; + } + + /** + * Uses Generics Maps a value in one range to its equivalent in a second range + * + * @param valueToMap value in the first range that needs to be converted + * @param in_min the minimum value for the original range + * @param in_max the maximum value for the original range + * @param out_min the minimum value for the target range + * @param out_max the maximum value for the target range + * @return the equivalent value of valueToMap in the target range + * @param + */ + public static T mapToRange( + T valueToMap, T in_min, T in_max, T out_min, T out_max) { + Number mapValue = (Number) valueToMap; + Number inMin = (Number) in_min; + Number inMax = (Number) in_max; + Number outMin = (Number) out_min; + Number outMax = (Number) out_max; + Number result; + if (isFloat(valueToMap)) { + result = + (Number) + ((mapValue.floatValue() - inMin.floatValue()) + * (outMax.floatValue() - outMin.floatValue()) + / (inMax.floatValue() - inMin.floatValue()) + + outMin.floatValue()); + } else { + result = + (Number) + ((mapValue.doubleValue() - inMin.doubleValue()) + * (outMax.doubleValue() - outMin.doubleValue()) + / (inMax.doubleValue() - inMin.doubleValue()) + + outMin.doubleValue()); + } + return (T) result; + } + + /** + * Constrains an integer between an upper and lower limit + * + * @param value + * @param lowBound + * @param highBound + * @return + */ + public static int constrainInt(int value, int lowBound, int highBound) { + return Math.max(lowBound, Math.min(highBound, value)); + } + + /** + * Constrains a Number between an upper and lower limit + * + * @param value + * @param lowBound + * @param highBound + * @return + * @param + */ + public static T constrainNumber(T value, T lowBound, T highBound) { + return (T) + (Number) + Math.max( + lowBound.doubleValue(), Math.min(highBound.doubleValue(), value.doubleValue())); + } +} diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index afce6c607d..a2950b0cc7 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -15,30 +15,25 @@ package net.rptools.lib.image; import com.twelvemonkeys.image.ResampleOp; -import java.awt.AlphaComposite; -import java.awt.Color; -import java.awt.Composite; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.MediaTracker; -import java.awt.Transparency; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ImageObserver; import java.awt.image.PixelGrabber; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.util.Arrays; import java.util.Map; import javax.imageio.ImageIO; -import javax.swing.ImageIcon; -import javax.swing.JPanel; +import javax.swing.*; +import net.rptools.lib.MathUtil; import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.model.*; +import net.rptools.maptool.util.ImageManager; +import net.rptools.parser.ParserException; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -47,55 +42,305 @@ * @author trevor */ public class ImageUtil { - private static final Logger log = LogManager.getLogger(); - public static final String HINT_TRANSPARENCY = "hintTransparency"; - - // TODO: perhaps look at reintroducing this later - // private static GraphicsConfiguration graphicsConfig = - // GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); - public static final FilenameFilter SUPPORTED_IMAGE_FILE_FILTER = (dir, name) -> { name = name.toLowerCase(); return Arrays.asList(ImageIO.getReaderFileSuffixes()).contains(name); }; + // TODO: perhaps look at reintroducing this later + // private static GraphicsConfiguration graphicsConfig = + // GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + private static final Logger log = LogManager.getLogger(); + // public static void setGraphicsConfiguration(GraphicsConfiguration config) { // graphicsConfig = config; // } // + private static final JPanel observer = new JPanel(); + private static final int[][] outlineNeighborMap = { + {0, -1, 100}, // N + {1, 0, 100}, // E + {0, 1, 100}, // S + {-1, 0, 100} // W + , + {-1, -1}, // NW + {1, -1}, // NE + {-1, 1}, // SW + {1, 1}, // SE + }; + private static RenderingHints renderingHintsQuality; + + public static BufferedImage replaceColor(BufferedImage src, int sourceRGB, int replaceRGB) { + for (int y = 0; y < src.getHeight(); y++) { + for (int x = 0; x < src.getWidth(); x++) { + int rawRGB = src.getRGB(x, y); + int rgb = rawRGB & 0xffffff; + int alpha = rawRGB & 0xff000000; + + if (rgb == sourceRGB) { + src.setRGB(x, y, alpha | replaceRGB); + } + } + } + return src; + } + + public static int negativeColourInt(int rgb) { + int r = 255 - ((rgb >> 16) & 0xFF); + int g = 255 - ((rgb >> 8) & 0xFF); + int b = 255 - (rgb & 0xFF); + int negativeRGB = (r << 16) | (g << 8) | b; + return negativeRGB; + } + + public static BufferedImage negativeImage(BufferedImage originalImage) { + // Get the dimensions of the image + int width = originalImage.getWidth(); + int height = originalImage.getHeight(); + // Create a new BufferedImage for the negative image + BufferedImage negativeImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + // Loop through each pixel of the original image and convert it to its negative + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int negativeRGB = negativeColourInt(originalImage.getRGB(x, y)); + negativeImage.setRGB(x, y, negativeRGB); + } + } + return negativeImage; + } + + /** + * Scales the provided image with the ZoneRenderer scale, the token footprint, and the token's + * layout scale factors. + * + * @param bi Image to scale + * @return Scaled bufferedImage + */ + public static BufferedImage getScaledTokenImage(BufferedImage bi, Token token, ZoneRenderer zr) { + Grid grid = zr.getZone().getGrid(); + double zoneS = zr.getScale(); + return getScaledTokenImage(bi, token, grid, zoneS); + } + + public static BufferedImage getScaledTokenImage( + BufferedImage img, Token token, Grid grid, double zoom) { + TokenFootprint footprint = token.getFootprint(grid); + Rectangle2D footprintBounds = footprint.getBounds(grid, new CellPoint(0, 0)); + double zoomS = zoom; + double fpS = + footprint + .getScale(); // except gridless, this should be 1 for footprints larger than the grid + // size + double fpW, fpH; + // multiply by zoom level to prevent mutliple scaling ops which lose definition + if (grid.equals(GridFactory.NONE)) { + fpW = fpH = grid.getSize() * fpS * zoomS; // all gridless are relative to the grid size + } else { + fpW = footprintBounds.getWidth() * fpS * zoomS; + fpH = footprintBounds.getHeight() * fpS * zoomS; + } + + double imgW = img.getWidth(); + double imgH = img.getHeight(); + double sXY = token.getSizeScale(); + double sX = token.getScaleX(); + double sY = token.getScaleY(); + // scale to fit image inside footprint bounds using the dimension that needs the most scaling, + // i.e. lowest ratio + double imageFootprintRatio = 1; + if (token.getShape() == Token.TokenShape.FIGURE && grid.isIsometric()) { + // uses double footprint height + imageFootprintRatio = Math.min(fpW / imgW, fpH * 2 / imgH); + } else { + imageFootprintRatio = Math.min(fpW / imgW, fpH / imgH); + } + // combine with token scale properties + if (sX != 1 || sY != 1 || sXY != 1 || imageFootprintRatio != 1) { + int outputWidth = (int) Math.ceil(imgW * sXY * sX * imageFootprintRatio); + int outputHeight = (int) Math.ceil(imgH * sXY * sY * imageFootprintRatio); + token.setWidth(outputWidth); + token.setHeight(outputHeight); + try { + return ImageUtil.scaleBufferedImage(img, outputWidth, outputHeight); + } catch (Exception e) { + log.debug(e.getLocalizedMessage(), e); + return img; + } + } else { + return img; + } + } + + /** + * Create a copy of the image that is compatible with the current graphics context + * + * @param img to use + * @return compatible BufferedImage + */ + public static BufferedImage createCompatibleImage(Image img) { + return createCompatibleImage(img, null); + } + + public static BufferedImage createCompatibleImage(int width, int height, int transparency) { + return new BufferedImage(width, height, transparency); + } + + /** + * Convert a BufferedImage to byte[] in the jpg format. + * + * @param image the buffered image. + * @return the byte[] of the image + * @throws IOException if the image cannot be written to the output stream. + */ + public static byte[] imageToBytes(BufferedImage image) throws IOException { + + // First try jpg, if it cant be converted to jpg try png + byte[] imageBytes = imageToBytes(image, "jpg"); + if (imageBytes.length > 0) { + return imageBytes; + } + + return imageToBytes(image, "png"); + } + + /** + * Convert a BufferedImage to byte[] in an given format. + * + * @param image the buffered image. + * @param format a String containing the informal name of the format. + * @return the byte[] of the image. + * @throws IOException if the image cannot be written to the output stream. + */ + public static byte[] imageToBytes(BufferedImage image, String format) throws IOException { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(10000); + ImageIO.write(image, format, outStream); + + return outStream.toByteArray(); + } + + public static void clearImage(BufferedImage image) { + if (image == null) { + return; + } + + Graphics2D g = null; + try { + g = (Graphics2D) image.getGraphics(); + Composite oldComposite = g.getComposite(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setComposite(oldComposite); + } finally { + if (g != null) { + g.dispose(); + } + } + } + + public static BufferedImage createOutline(BufferedImage sourceImage, Color color) { + if (sourceImage == null) { + return null; + } + BufferedImage image = + new BufferedImage( + sourceImage.getWidth() + 2, sourceImage.getHeight() + 2, Transparency.BITMASK); + + for (int row = 0; row < image.getHeight(); row++) { + for (int col = 0; col < image.getWidth(); col++) { + int sourceX = col - 1; + int sourceY = row - 1; + + // Pixel under current location + if (sourceX >= 0 + && sourceY >= 0 + && sourceX <= sourceImage.getWidth() - 1 + && sourceY <= sourceImage.getHeight() - 1) { + int sourcePixel = sourceImage.getRGB(sourceX, sourceY); + if (sourcePixel >> 24 != 0) { + // Not an empty pixel, don't overwrite it + continue; + } + } + for (int[] neighbor : outlineNeighborMap) { + int x = sourceX + neighbor[0]; + int y = sourceY + neighbor[1]; + + if (x >= 0 + && y >= 0 + && x <= sourceImage.getWidth() - 1 + && y <= sourceImage.getHeight() - 1 + && (sourceImage.getRGB(x, y) >> 24) != 0) { + image.setRGB(col, row, color.getRGB()); + break; + } + } + } + } + return image; + } + + /** + * Returns the provided image flipped according to the token's flip-states + * + * @param image The image to be processed + * @param token The token containing the flip-states + * @return A modified image, or the original image if no processing performed. + */ + public static BufferedImage flipTokenImage(BufferedImage image, Token token) { + int direction = 0 + (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); + image = flipCartesian(image, direction); + if (token.isFlippedIso()) { + return IsometricGrid.isoImage(image); + } else { + return image; + } + } + /** * Load the image. Does not create a graphics configuration compatible version. * * @param file the file with the image in it - * @throws IOException when the image can't be read in the file * @return an {@link Image} from the content of the file + * @throws IOException when the image can't be read in the file */ public static Image getImage(File file) throws IOException { return bytesToImage(FileUtils.readFileToByteArray(file), file.getCanonicalPath()); } /** - * Load the image in the classpath. Does not create a graphics configuration compatible version. + * Converts a byte array into an {@link Image} instance. * - * @param image the resource name of the image file - * @throws IOException when the image can't be read in the file - * @return an {@link Image} from the content of the file + * @param imageBytes bytes to convert + * @param imageName name of image + * @return the image + * @throws IOException if image could not be loaded */ - public static Image getImage(String image) throws IOException { - ByteArrayOutputStream dataStream = new ByteArrayOutputStream(8192); - - int bite; - InputStream inStream = ImageUtil.class.getClassLoader().getResourceAsStream(image); - if (inStream == null) { - throw new IOException("Image not found: " + image); + public static Image bytesToImage(byte[] imageBytes, String imageName) throws IOException { + if (imageBytes == null) { + throw new IOException("Could not load image - no data provided"); } - inStream = new BufferedInputStream(inStream); - while ((bite = inStream.read()) >= 0) { - dataStream.write(bite); + boolean interrupted = false; + Throwable exception = null; + Image image; + image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + MediaTracker tracker = new MediaTracker(observer); + tracker.addImage(image, 0); + do { + try { + interrupted = false; + tracker.waitForID(0); // This is the only method that throws an exception + } catch (InterruptedException t) { + interrupted = true; + } catch (Throwable t) { + exception = t; + } + } while (interrupted); + if (image == null) { + throw new IOException("Could not load image " + imageName, exception); } - return bytesToImage(dataStream.toByteArray(), image); + return image; } public static BufferedImage getCompatibleImage(String image) throws IOException { @@ -107,16 +352,6 @@ public static BufferedImage getCompatibleImage(String image, Map return createCompatibleImage(getImage(image), hints); } - /** - * Create a copy of the image that is compatible with the current graphics context - * - * @param img to use - * @return compatible BufferedImage - */ - public static BufferedImage createCompatibleImage(Image img) { - return createCompatibleImage(img, null); - } - public static BufferedImage createCompatibleImage(Image img, Map hints) { if (img == null) { return null; @@ -124,8 +359,26 @@ public static BufferedImage createCompatibleImage(Image img, Map return createCompatibleImage(img, img.getWidth(null), img.getHeight(null), hints); } - public static BufferedImage createCompatibleImage(int width, int height, int transparency) { - return new BufferedImage(width, height, transparency); + /** + * Load the image in the classpath. Does not create a graphics configuration compatible version. + * + * @param image the resource name of the image file + * @return an {@link Image} from the content of the file + * @throws IOException when the image can't be read in the file + */ + public static Image getImage(String image) throws IOException { + ByteArrayOutputStream dataStream = new ByteArrayOutputStream(8192); + + int bite; + InputStream inStream = ImageUtil.class.getClassLoader().getResourceAsStream(image); + if (inStream == null) { + throw new IOException("Image not found: " + image); + } + inStream = new BufferedInputStream(inStream); + while ((bite = inStream.read()) >= 0) { + dataStream.write(bite); + } + return bytesToImage(dataStream.toByteArray(), image); } /** @@ -247,145 +500,106 @@ public static int pickBestTransparency(BufferedImage image) { return foundTransparent ? Transparency.BITMASK : Transparency.OPAQUE; } + public static double getIsoFigureHeightOffset(Token token, Rectangle2D footprintBounds) { + double scale = getIsoFigureScaleFactor(token, footprintBounds); + double th = token.getHeight() * scale; + return footprintBounds.getHeight() - th; + } + /** - * Convert a BufferedImage to byte[] in the jpg format. + * Use width ratio unless height exceeds double footprint height * - * @param image the buffered image. - * @return the byte[] of the image - * @throws IOException if the image cannot be written to the output stream. + * @param token + * @param footprintBounds + * @return double */ - public static byte[] imageToBytes(BufferedImage image) throws IOException { - - // First try jpg, if it cant be converted to jpg try png - byte[] imageBytes = imageToBytes(image, "jpg"); - if (imageBytes.length > 0) { - return imageBytes; - } - - return imageToBytes(image, "png"); + public static double getIsoFigureScaleFactor(Token token, Rectangle2D footprintBounds) { + return Math.min( + footprintBounds.getWidth() / token.getWidth(), + footprintBounds.getHeight() * 2 / token.getHeight()); } /** - * Convert a BufferedImage to byte[] in an given format. + * Get the offset values required to align an image within specified bounds * - * @param image the buffered image. - * @param format a String containing the informal name of the format. - * @return the byte[] of the image. - * @throws IOException if the image cannot be written to the output stream. + * @param imgSize Dimension + * @param footprintBounds Rectangle + * @return int array of length 2 [x,y] */ - public static byte[] imageToBytes(BufferedImage image, String format) throws IOException { - ByteArrayOutputStream outStream = new ByteArrayOutputStream(10000); - ImageIO.write(image, format, outStream); - - return outStream.toByteArray(); + public static double[] getImageAlignmentOffsets(BufferedImage image, Rectangle footprintBounds) { + double[] offsets = new double[2]; + offsets[0] = + image.getWidth() < footprintBounds.width + ? (footprintBounds.width - image.getWidth()) / 2 + : image.getWidth() > footprintBounds.width + ? -(image.getWidth() - footprintBounds.width) / 2 + : 0; + offsets[1] = + image.getHeight() < footprintBounds.height + ? (footprintBounds.height - image.getHeight()) / 2 + : image.getHeight() > footprintBounds.height + ? -(image.getHeight() - footprintBounds.height) / 2 + : 0; + return offsets; } - private static final JPanel observer = new JPanel(); - /** - * Converts a byte array into an {@link Image} instance. + * Gets the token image; applies flipping, scaling, and image rotation, but not facing. * - * @param imageBytes bytes to convert - * @param imageName name of image - * @return the image - * @throws IOException if image could not be loaded + * @param token + * @param zr + * @return modified image */ - public static Image bytesToImage(byte[] imageBytes, String imageName) throws IOException { - if (imageBytes == null) { - throw new IOException("Could not load image - no data provided"); - } - boolean interrupted = false; - Throwable exception = null; - Image image; - image = ImageIO.read(new ByteArrayInputStream(imageBytes)); - MediaTracker tracker = new MediaTracker(observer); - tracker.addImage(image, 0); - do { - try { - interrupted = false; - tracker.waitForID(0); // This is the only method that throws an exception - } catch (InterruptedException t) { - interrupted = true; - } catch (Throwable t) { - exception = t; - } - } while (interrupted); - if (image == null) { - throw new IOException("Could not load image " + imageName, exception); - } - return image; - } + public static BufferedImage getTokenRenderImage(Token token, ZoneRenderer zr) { + BufferedImage image = getTokenImage(token, zr); - public static void clearImage(BufferedImage image) { - if (image == null) { - return; - } - - Graphics2D g = null; - try { - g = (Graphics2D) image.getGraphics(); - Composite oldComposite = g.getComposite(); - g.setComposite(AlphaComposite.Clear); - g.fillRect(0, 0, image.getWidth(), image.getHeight()); - g.setComposite(oldComposite); - } finally { - if (g != null) { - g.dispose(); - } + int flipDirection = 0 + (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); + image = flipCartesian(image, flipDirection); + if (token.isFlippedIso() && zr.getZone().getGrid().isIsometric()) { + image = flipIsometric(image, true); } - } - private static final int[][] outlineNeighborMap = { - {0, -1, 100}, // N - {1, 0, 100}, // E - {0, 1, 100}, // S - {-1, 0, 100} // W - , - {-1, -1}, // NW - {1, -1}, // NE - {-1, 1}, // SW - {1, 1}, // SE - }; + image = getScaledTokenImage(image, token, zr); - public static BufferedImage createOutline(BufferedImage sourceImage, Color color) { - if (sourceImage == null) { - return null; + if (token.getImageRotation() != 0) { + image = rotateImage(image, token.getImageRotation()); } - BufferedImage image = - new BufferedImage( - sourceImage.getWidth() + 2, sourceImage.getHeight() + 2, Transparency.BITMASK); - - for (int row = 0; row < image.getHeight(); row++) { - for (int col = 0; col < image.getWidth(); col++) { - int sourceX = col - 1; - int sourceY = row - 1; - - // Pixel under current location - if (sourceX >= 0 - && sourceY >= 0 - && sourceX <= sourceImage.getWidth() - 1 - && sourceY <= sourceImage.getHeight() - 1) { - int sourcePixel = sourceImage.getRGB(sourceX, sourceY); - if (sourcePixel >> 24 != 0) { - // Not an empty pixel, don't overwrite it - continue; - } - } - for (int[] neighbor : outlineNeighborMap) { - int x = sourceX + neighbor[0]; - int y = sourceY + neighbor[1]; + return image; + } - if (x >= 0 - && y >= 0 - && x <= sourceImage.getWidth() - 1 - && y <= sourceImage.getHeight() - 1 - && (sourceImage.getRGB(x, y) >> 24) != 0) { - image.setRGB(col, row, color.getRGB()); - break; + /** + * Checks to see if token has an image table and references that if the token has a facing + * otherwise uses basic image + * + * @param token the token to get the image from. + * @return BufferedImage + */ + public static BufferedImage getTokenImage(Token token, ZoneRenderer zr) { + BufferedImage image = null; + // Get the basic image + if (token.getHasImageTable() + && token.hasFacing() + && token.getImageTableName() != null + && zr.getZone().getGrid().isIsometric()) { + LookupTable lookupTable = + MapTool.getCampaign().getLookupTableMap().get(token.getImageTableName()); + if (lookupTable != null) { + try { + LookupTable.LookupEntry result = + lookupTable.getLookup(Integer.toString(token.getFacing())); + if (result != null) { + image = ImageManager.getImage(result.getImageId(), zr); } + } catch (ParserException p) { + // do nothing } } } + + if (image == null) { + // Adds zr as observer so we can repaint once the image is ready. Fixes #1700. + image = ImageManager.getImage(token.getImageAssetId(), zr); + } return image; } @@ -396,13 +610,17 @@ public static BufferedImage createOutline(BufferedImage sourceImage, Color color * @param direction 0-nothing, 1-horizontal, 2-vertical, 3-both * @return flipped BufferedImage */ - public static BufferedImage flip(BufferedImage image, int direction) { - BufferedImage workImage = - new BufferedImage(image.getWidth(), image.getHeight(), image.getTransparency()); - + public static BufferedImage flipCartesian(BufferedImage image, int direction) { boolean flipHorizontal = (direction & 1) == 1; boolean flipVertical = (direction & 2) == 2; + if (!flipHorizontal && !flipVertical) { + return image; + } + + BufferedImage workImage = + new BufferedImage(image.getWidth(), image.getHeight(), image.getTransparency()); + int workW = image.getWidth() * (flipHorizontal ? -1 : 1); int workH = image.getHeight() * (flipVertical ? -1 : 1); int workX = flipHorizontal ? image.getWidth() : 0; @@ -415,21 +633,45 @@ public static BufferedImage flip(BufferedImage image, int direction) { return workImage; } - public static ImageIcon scaleImage(ImageIcon icon, int w, int h) { - int nw = icon.getIconWidth(); - int nh = icon.getIconHeight(); - - if (icon.getIconWidth() > w) { - nw = w; - nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); + public static BufferedImage flipIsometric(BufferedImage image, boolean toRhombus) { + BufferedImage workImage; + boolean isSquished = + MathUtil.inTolerance(image.getHeight(), image.getWidth() / 2d, image.getHeight() * 0.05); + if (image.getWidth() != image.getHeight()) { + int maxDim = Math.max(image.getWidth(), image.getHeight()); + int w = 1, h = 1; + if (toRhombus) { + // make it square and centred + w = h = maxDim; + } else { + if (!isSquished) { + w = maxDim; + h = (int) Math.ceil(maxDim / 2d); + } else { + w = image.getWidth(); + w = (int) Math.ceil(image.getWidth() / 2d); + } + } + workImage = new BufferedImage(w, h, image.getTransparency()); + Graphics2D wig = workImage.createGraphics(); + wig.drawImage( + image, + (workImage.getWidth() - image.getWidth()) / 2, + (workImage.getHeight() - image.getHeight()) / 2, + image.getWidth(), + image.getHeight(), + null); + wig.dispose(); + image = workImage; } - - if (nh > h) { - nh = h; - nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); + if (toRhombus) { + image = rotateImage(image, 45); + image = scaleBufferedImage(image, image.getWidth(), image.getHeight() / 2); + } else { + image = scaleBufferedImage(image, image.getWidth(), image.getWidth()); + image = rotateImage(image, -45); } - - return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_DEFAULT)); + return image; } /** @@ -445,4 +687,60 @@ public static BufferedImage scaleBufferedImage(BufferedImage image, int width, i new ResampleOp(width, height, AppPreferences.renderQuality.get().getResampleOpFilter()); return resampleOp.filter(image, null); } + + public static ImageIcon scaleImageIcon(ImageIcon icon, int w, int h) { + int nw = icon.getIconWidth(); + int nh = icon.getIconHeight(); + + if (icon.getIconWidth() > w) { + nw = w; + nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); + } + + if (nh > h) { + nh = h; + nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); + } + + return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_DEFAULT)); + } + + public static RenderingHints getRenderingHintsQuality() { + if (renderingHintsQuality == null) { + renderingHintsQuality = + new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + renderingHintsQuality.put( + RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + renderingHintsQuality.put( + RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + renderingHintsQuality.put( + RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + renderingHintsQuality.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); + } + return renderingHintsQuality; + } + + public static BufferedImage rotateImage(BufferedImage img, double degrees) { + double rads = Math.toRadians(degrees); + double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads)); + int w = img.getWidth(); + int h = img.getHeight(); + int newWidth = (int) Math.floor(w * cos + h * sin); + int newHeight = (int) Math.floor(h * cos + w * sin); + + BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = rotated.createGraphics(); + g2d.setRenderingHints(getRenderingHintsQuality()); + AffineTransform at = new AffineTransform(); + at.translate((newWidth - w) / 2.0, (newHeight - h) / 2.0); + + double x = w / 2.0; + double y = h / 2.0; + + at.rotate(rads, x, y); + g2d.setTransform(at); + g2d.drawImage(img, 0, 0, null); + g2d.dispose(); + return rotated; + } } diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenImage.java b/src/main/java/net/rptools/maptool/client/functions/TokenImage.java index 0ed182c66f..7fbbe226bc 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenImage.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenImage.java @@ -110,7 +110,7 @@ public Object childEvaluate( FunctionUtil.paramAsFloat(functionName, args, 0, true); token = FunctionUtil.getTokenFromParam(resolver, functionName, args, 1, 2); - MapTool.serverCommand().updateTokenProperty(token, Token.Update.setTokenOpacity, strOpacity); + MapTool.serverCommand().updateTokenProperty(token, Token.Update.setOpacity, strOpacity); return token.getOpacity(); } diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java index 8e1fbd9150..b78a0a4a5f 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java @@ -713,7 +713,7 @@ public Object childEvaluate( if (functionName.equalsIgnoreCase("isFlippedIso")) { FunctionUtil.checkNumberParam(functionName, parameters, 0, 2); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 0, 1); - return token.getIsFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; + return token.isFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; } /* @@ -922,7 +922,7 @@ public Object childEvaluate( FunctionUtil.checkNumberParam(functionName, parameters, 0, 2); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 0, 1); MapTool.serverCommand().updateTokenProperty(token, Token.Update.flipIso); - return token.getIsFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; + return token.isFlippedIso() ? BigDecimal.ONE : BigDecimal.ZERO; } /* diff --git a/src/main/java/net/rptools/maptool/client/swing/SpinnerSliderPaired.java b/src/main/java/net/rptools/maptool/client/swing/SpinnerSliderPaired.java new file mode 100644 index 0000000000..1035d777e2 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/swing/SpinnerSliderPaired.java @@ -0,0 +1,567 @@ +/* + * 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.swing; + +import java.beans.*; +import java.text.ParseException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.SwingPropertyChangeSupport; +import net.rptools.lib.MathUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +/** + * Utility class for linking a numeric JSpinner, JSlider, and a variable. The slider and spinner are + * tied together with the class NumericModel. It holds delegates for the two component models which + * update each other.The default relationship between spinner value and slider value is 1:1 and + * utilises a Functional Interface to translate values between the two. This can be customised by + * setting your own Functions, e.g. an example for 1:100 + * + *

+ * Function<Number, Integer> spinnerToSlider =
+ * number -> ((Number) number.doubleValue * 100).intValue
+ * Function<Integer, Number> sliderToSpinner = + * number -> ((Number) i).doubleValue()/100d + *
+ * + *

Similarly:
+ * Providing a Consumer<Number> for a linked property will result in the property + * being updated with the spinner value.
+ * Providing a Supplier<Number> for a linked property can be used to update the + * spinner value by calling update(). PropertyChangeSupport and VetoableChangeSupport + * have been implemented on the spinner and slider models for the methods;
+ * setMinimum,
+ * setMaximum, and
+ * setValue. + */ +public class SpinnerSliderPaired { + // constructors + public SpinnerSliderPaired(JSpinner spinner, JSlider slider) { + this(spinner, slider, null); + } + + public SpinnerSliderPaired(JSpinner spinner, JSlider slider, Consumer propertySetter) { + this(spinner, slider, propertySetter, (n) -> n.intValue(), (i) -> ((Number) i).doubleValue()); + log.debug("spinner-slider pair using default relationship."); + } + + public SpinnerSliderPaired( + JSpinner spinner, + JSlider slider, + Consumer propertySetter, + Function spinnerToSlider, + Function sliderToSpinner) { + // set funcitional relationship + setSpinnerToSlider(spinnerToSlider); + setSliderToSpinner(sliderToSpinner); + // set the controls + setLinkedSpinner(spinner); + setLinkedSlider(slider); + // set the property setter + setPropertySetter(propertySetter); + + commonModel.setDelegateValues(); + getLinkedSlider().setModel(commonModel.sliderModelDelegate); + getLinkedSpinner().setModel(commonModel.spinnerModelDelegate); + + log.debug("new spinner-slider pair: " + this.toString()); + } + + // @formatter:off + // spotless:off + private static final Logger log = LogManager.getLogger(SpinnerSliderPaired.class); + // Property Change Support + protected SwingPropertyChangeSupport pcs = new SwingPropertyChangeSupport(this); + protected VetoableChangeSupport vcs = new VetoableChangeSupport(this); + public void addVetoableChangeListener(VetoableChangeListener listener) { vcs.addVetoableChangeListener(listener); } + protected void removeVetoableChangeListener(VetoableChangeListener listener){ vcs.addVetoableChangeListener(listener); } + public void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); } + protected void removePropertyChangeListener(PropertyChangeListener listener) { pcs.removePropertyChangeListener(listener); } + + // controls + private transient JSpinner linkedSpinner; + private transient JSlider linkedSlider; + public JSpinner getLinkedSpinner(){ return linkedSpinner; } + public JSlider getLinkedSlider(){ return linkedSlider; } + private void setLinkedSlider(@NotNull JSlider slider){ + this.linkedSlider = slider; + getLinkedSlider().addMouseWheelListener( + e -> { incrementValue((int) Math.round(e.getPreciseWheelRotation())); + }); + } + private void setLinkedSpinner(JSpinner spinner){ + this.linkedSpinner = spinner; + getLinkedSpinner().addChangeListener(spinnerEditListener); + getLinkedSpinner().addMouseWheelListener( + e -> { incrementValue( e.getPreciseWheelRotation()); + }); + } + // option to set the spinner to loop/wrap at end values + private boolean spinnerWraps = false; + public void setSpinnerWraps(boolean b){ this.spinnerWraps = b; } + + // property setter + private Consumer propertySetter; + private Supplier propertyGetter; + private String propertyName = "Property"; + public Consumer getPropertySetter() { return propertySetter; } + public Supplier getPropertyGetter() { return propertyGetter; } + public void setPropertySetter(Consumer propertySetter) {this.propertySetter = propertySetter; } + public void setPropertyGetter(Supplier propertyGetter) {this.propertyGetter = propertyGetter; } + public void setPropertyName(String s){ this.propertyName = s; } + private void setProperty(Number n){ + if(propertyGetter != null){ + if(propertyGetter.get().doubleValue() != n.doubleValue() && propertySetter != null){ + propertySetter.accept(n); + log.debug(propertyName + " set to " + n); + } + } + } + public void update(){ if(propertyGetter != null){ setValue(propertyGetter.get()); } } + // Instance of NumericModel that ties the two controls together + private NumericModel commonModel = new NumericModel(); + // functions that define the relationship between spinner and slider values + private Function sliderToSpinner; + private Function spinnerToSlider; + public Function getSpinnerToSlider() { return spinnerToSlider; } + public void setSpinnerToSlider(Function function) { spinnerToSlider = function; } + public Function getSliderToSpinner() { return sliderToSpinner; } + public void setSliderToSpinner(Function function) { sliderToSpinner = function; } + + // public methods pointing to model methods + public Number getNextValue(){ return commonModel.getNextValue(); } + public Number getPreviousValue(){ return commonModel.getPreviousValue(); } + public int getSliderValue(){ return commonModel.getNumber(true).intValue(); } + public double getSpinnerValue(){ return commonModel.getNumber(false).doubleValue(); } + public void incrementValue(Number n){ commonModel.incrementValue(n);} + public void setMaximum(Number n){ commonModel.setMaximum(n);} + public void setMinimum(Number n){ commonModel.setMinimum(n);} + public void setValue(Number n){ commonModel.setValue(n);} + //@formatter:on + // spotless:on + // The shared model - two delegates controlled from above + public class NumericModel { + public NumericModel() {} + + // initialise delegates with linked spinner values + private void setDelegateValues() { + if (getLinkedSpinner() == null) { + return; + } + if (spinnerToSlider == null) { // default 1:1 relationship + sliderToSpinner = i -> ((Number) i).doubleValue(); + spinnerToSlider = n -> n.intValue(); + } + SpinnerNumberModel spinModel = (SpinnerNumberModel) getLinkedSpinner().getModel(); + spinnerModelDelegate = + new NumericSpinnerModel( + spinModel.getNumber().doubleValue(), + ((Number) spinModel.getMinimum()).doubleValue(), + ((Number) spinModel.getMaximum()).doubleValue(), + spinModel.getStepSize().doubleValue()); + sliderModelDelegate = + new NumericSliderModel( + spinnerToSlider.apply(spinnerModelDelegate.getNumber()), + 0, + spinnerToSlider.apply(((Number) spinnerModelDelegate.getMinimum())), + spinnerToSlider.apply(((Number) spinnerModelDelegate.getMaximum()))); + } + + // a method for every flavour + private void incrementValue(Number delta) { + PropertyChangeEvent pce = new PropertyChangeEvent(this, "increment", getNumber(false), delta); + try { + vcs.fireVetoableChange(pce); + spinnerModelDelegate.increment(delta); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage(), e); + } + } + + private void setStepSize(Number n) { + PropertyChangeEvent pce = new PropertyChangeEvent(this, "stepsize", getNumber(false), n); + try { + vcs.fireVetoableChange(pce); + spinnerModelDelegate.setStepSize(n); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage(), e); + } + } + + private void setValue(Number n) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n); + try { + vcs.fireVetoableChange(pce); + if (MathUtil.isInt(n)) { + sliderModelDelegate.setValue(n.intValue()); + } else { + spinnerModelDelegate.setValue(n); + } + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage(), e); + } + } + + private void setMaximum(Number n) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n); + try { + vcs.fireVetoableChange(pce); + if (MathUtil.isInt(n)) { + sliderModelDelegate.setMaximum(n.intValue()); + } else { + spinnerModelDelegate.setMaximum((Comparable) n); + } + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage(), e); + } + } + + private void setMinimum(Number n) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n); + try { + vcs.fireVetoableChange(pce); + if (MathUtil.isInt(n)) { + sliderModelDelegate.setMinimum(n.intValue()); + } else { + spinnerModelDelegate.setMinimum((Comparable) n); + } + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage(), e); + } + } + + private Number getNextValue() { + return (Number) spinnerModelDelegate.getNextValue(); + } + + private Number getPreviousValue() { + return (Number) spinnerModelDelegate.getPreviousValue(); + } + + private T getNumber(boolean asInt) { + if (asInt) { + return (T) spinnerToSlider.apply(spinnerModelDelegate.getNumber()); + } else { + return (T) spinnerModelDelegate.getNumber(); + } + } + + // Delegate classes with aditional setters and normal setters redirected + public NumericSpinnerModel spinnerModelDelegate = new NumericSpinnerModel(0d, 0d, 100d, 1d); + + private class NumericSpinnerModel extends SpinnerNumberModel { + public NumericSpinnerModel(double value, double minimum, double maximum, double stepSize) { + super(value, minimum, maximum, stepSize); + } + + public void setMax(@NotNull Number n) { + super.setMaximum(n.doubleValue()); + sliderModelDelegate.setMaximum(spinnerToSlider.apply((Number) super.getMaximum())); + } + + public void setMin(@NotNull Number n) { + super.setMinimum(n.doubleValue()); + sliderModelDelegate.setMinimum(spinnerToSlider.apply((Number) super.getMinimum())); + } + + public void setVal(@NotNull Number n) { + super.setValue(n.doubleValue()); + setProperty(n); + sliderModelDelegate.setValue(spinnerToSlider.apply(super.getNumber())); + } + + public void setStep(@NotNull Number n) { + super.setStepSize(n); + } + + public void incr(@NotNull Number n) { + double newVal = getNumber().doubleValue() + n.doubleValue(); + double max = ((Number) getMaximum()).doubleValue(); + double min = ((Number) getMinimum()).doubleValue(); + if (spinnerWraps && (newVal < min || newVal > max)) { + if (newVal < min) { + newVal = newVal - min + max; + } else { + newVal = newVal - max + min; + } + } else { + newVal = Math.min(max, Math.max(newVal, min)); + } + setValue(newVal); + } + + public void increment(@NotNull Number n) { + n = MathUtil.doublePrecision(n.doubleValue(), 4); + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "increment", super.getNumber(), n); + vcs.fireVetoableChange(pce); + incr(n); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + + @Override + public Object getNextValue() { + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "increment", super.getNumber(), super.getStepSize()); + vcs.fireVetoableChange(pce); + incr(getStepSize()); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + return getNumber(); + } + + @Override + public Object getPreviousValue() { + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "increment", super.getNumber(), super.getStepSize()); + vcs.fireVetoableChange(pce); + incr(-getStepSize().doubleValue()); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + return super.getNumber(); + } + + @Override + public void setMinimum(Comparable minimum) { + minimum = MathUtil.doublePrecision(((Number) minimum).doubleValue(), 4); + if (((Number) this.getMinimum()).doubleValue() != ((Number) minimum).doubleValue()) { + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "minimum", super.getNumber(), minimum); + vcs.fireVetoableChange(pce); + setMin((Number) minimum); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public void setMaximum(Comparable maximum) { + maximum = MathUtil.doublePrecision(((Number) maximum).doubleValue(), 4); + if (((Number) this.getMaximum()).doubleValue() != ((Number) maximum).doubleValue()) { + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "maximum", super.getNumber(), maximum); + vcs.fireVetoableChange(pce); + setMax((Number) maximum); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public void setValue(Object value) { + value = MathUtil.doublePrecision(((Number) value).doubleValue(), 4); + if (this.getNumber().doubleValue() != ((Number) value).doubleValue()) { + try { + PropertyChangeEvent pce = + new PropertyChangeEvent(this, "value", super.getNumber(), value); + vcs.fireVetoableChange(pce); + setVal((Number) value); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public String toString() { + return "spinnerModelDelegate{" + + "min=" + + getMinimum() + + ", max=" + + getMaximum() + + ", val=" + + getValue() + + ", stepSize=" + + getStepSize() + + '}'; + } + } + ; + + public NumericSliderModel sliderModelDelegate = new NumericSliderModel(0, 0, 0, 100); + + private class NumericSliderModel extends DefaultBoundedRangeModel { + public NumericSliderModel(int value, int extent, int min, int max) { + super(value, extent, min, max); + } + + public void setMax(@NotNull Number n) { + super.setMaximum(n.intValue()); + spinnerModelDelegate.setMaximum(sliderToSpinner.apply(n.intValue()).doubleValue()); + } + + public void setMin(@NotNull Number n) { + super.setMinimum(n.intValue()); + spinnerModelDelegate.setMinimum(sliderToSpinner.apply(n.intValue()).doubleValue()); + } + + public void setVal(@NotNull Number n) { + super.setValue(n.intValue()); + spinnerModelDelegate.setValue(sliderToSpinner.apply(n.intValue()).doubleValue()); + } + + @Override + public void setValue(int n) { + if (this.getValue() != n) { + try { + PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getValue(), n); + vcs.fireVetoableChange(pce); + setVal((Number) n); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public void setMinimum(int n) { + if (this.getValue() != n) { + try { + PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getMinimum(), n); + vcs.fireVetoableChange(pce); + setMin((Number) n); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public void setMaximum(int n) { + if (this.getValue() != n) { + try { + PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getMaximum(), n); + vcs.fireVetoableChange(pce); + setMax((Number) n); + pcs.firePropertyChange(pce); + } catch (PropertyVetoException e) { + log.info(e.getMessage()); + } + } + } + + @Override + public String toString() { + return "sliderModelDelegate{" + + "min=" + + super.getMinimum() + + ", max=" + + super.getMaximum() + + ", val=" + + super.getValue() + + ", extent=" + + super.getExtent() + + ", isAdjusting=" + + super.getValueIsAdjusting() + + '}'; + } + } + ; + + // spotless:on + // @formatter:on + @Override + public String toString() { + return "NumericModel{" + + spinnerModelDelegate.toString() + + ", " + + sliderModelDelegate.toString() + + '}'; + } + } + + @Override + public String toString() { + return "SpinnerSliderPaired{" + + "spinnerName=" + + getLinkedSpinner().getName() + + ", sliderName=" + + getLinkedSlider().getName() + + ", propertySetterSet=" + + (propertySetter != null) + + ", sliderToSpinner(0)=" + + sliderToSpinner.apply(0) + + ", sliderToSpinner(1)=" + + sliderToSpinner.apply(1) + + ", spinnerToSlider(0)=" + + spinnerToSlider.apply(0) + + ", spinnerToSlider(1)=" + + spinnerToSlider.apply(1) + + ", spinnerToSlider(0)=" + + spinnerToSlider.apply(0) + + ", sliderToSpinner(" + + spinnerToSlider.apply(0) + + ")=" + + sliderToSpinner.apply(spinnerToSlider.apply(0)) + + ", spinnerToSlider(1)=" + + spinnerToSlider.apply(1) + + ", sliderToSpinner(" + + spinnerToSlider.apply(1) + + ")=" + + sliderToSpinner.apply(spinnerToSlider.apply(1)) + + '}'; + } + + private ChangeListener spinnerEditListener = + new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + JSpinner spinner = (JSpinner) e.getSource(); + try { + spinner.commitEdit(); + } catch (ParseException pe) { + // Edited value is invalid, revert the spinner to the last valid value, + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.NumberEditor) { + ((JSpinner.NumberEditor) editor).getTextField().setValue(spinner.getValue()); + } + 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 dbe39f0b2d..5c8d1ed8a5 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 @@ -55,7 +55,7 @@ protected BooleanTokenOverlay(String aName) { public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, Object value) { if (FunctionUtil.getBooleanValue(value)) { // Apply Alpha Transparency - float opacity = token.getTokenOpacity(); + float opacity = token.getOpacity(); if (opacity < 1.0f) g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java index e5ffed9a83..15f06a3ba6 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java @@ -199,6 +199,26 @@ public void closeDialog() { super.closeDialog(); } }; + getTokenLayoutPanel().reset(token); + getFlippedX() + .addChangeListener( + e -> + getTokenLayoutPanel() + .getHelper() + .setTokenFlipX(((JCheckBox) e.getSource()).isSelected())); + getFlippedY() + .addChangeListener( + e -> + getTokenLayoutPanel() + .getHelper() + .setTokenFlipY(((JCheckBox) e.getSource()).isSelected())); + getFlippedIso() + .addChangeListener( + e -> + getTokenLayoutPanel() + .getHelper() + .setTokenFlipIso(((JCheckBox) e.getSource()).isSelected())); + getTokenTopologyPanel().reset(token); bind(token); @@ -339,7 +359,10 @@ public void bind(final Token token) { // OTHER getShapeCombo().setSelectedItem(token.getShape()); setSizeCombo(token); - + getSnapToGrid().setSelected(token.isSnapToGrid()); + getFlippedIso().setSelected(token.isFlippedIso()); + getFlippedX().setSelected(token.isFlippedX()); + getFlippedY().setSelected(token.isFlippedY()); // Updates the Property Type list. updatePropertyTypeCombo(); @@ -361,7 +384,7 @@ public void bind(final Token token) { getTokenLayoutPanel().setToken(token); getImageTableCombo().setSelectedItem(token.getImageTableName()); getTokenOpacitySlider() - .setValue(new BigDecimal(token.getTokenOpacity()).multiply(new BigDecimal(100)).intValue()); + .setValue(new BigDecimal(token.getOpacity()).multiply(new BigDecimal(100)).intValue()); getTerrainModifier().setText(Double.toString(token.getTerrainModifier())); getTerrainModifierOperationComboBox().setSelectedItem(token.getTerrainModifierOperation()); @@ -696,7 +719,7 @@ public JComboBox getImageTableCombo() { } public void initTokenOpacitySlider() { - getTokenOpacitySlider().addChangeListener(new SliderListener()); + getTokenOpacitySlider().addChangeListener(new OpacitySliderListener()); } public JSlider getTokenOpacitySlider() { @@ -721,7 +744,11 @@ public JList getTerrainModifiersIgnoredList() { public void setUniqueLightSourcesEnabled(boolean enabled) { getUniqueLightSourcesTextPane().setEnabled(enabled); - getLabel("uniqueLightSourcesLabel").setEnabled(enabled); + getUniqueLightSourcesPanel().setEnabled(enabled); + } + + public JPanel getUniqueLightSourcesPanel() { + return (JPanel) getComponent("uniqueLightSourcesPanel"); } public JTextPane getUniqueLightSourcesTextPane() { @@ -747,7 +774,7 @@ public void initOKButton() { public boolean commit() { Token token = getModel(); - if (getNameField().getText().equals("")) { + if (getNameField().getText().isEmpty()) { MapTool.showError("msg.error.emptyTokenName"); return false; } @@ -784,7 +811,7 @@ public boolean commit() { token.setPropertyType((String) getPropertyTypeCombo().getSelectedItem()); token.setSightType((String) getSightTypeCombo().getSelectedItem()); token.setImageTableName((String) getImageTableCombo().getSelectedItem()); - token.setTokenOpacity( + token.setOpacity( new BigDecimal(getTokenOpacitySlider().getValue()) .divide(new BigDecimal(100)) .floatValue()); @@ -908,8 +935,9 @@ public boolean commit() { token.setPortraitImage(getPortraitPanel().getImageId()); // LAYOUT - token.setSizeScale(getTokenLayoutPanel().getSizeScale()); - token.setAnchor(getTokenLayoutPanel().getAnchorX(), getTokenLayoutPanel().getAnchorY()); + getTokenLayoutPanel().getHelper().commitChanges(token); + + token.setSnapToGrid(getSnapToGrid().isSelected()); // TOPOLOGY for (final var type : Zone.TopologyType.values()) { @@ -940,10 +968,10 @@ public boolean commit() { MapTool.getFrame().resetTokenPanels(); // Jamz: TODO check if topology changed on token first - MapTool.getFrame() - .getCurrentZoneRenderer() - .getZone() - .tokenMaskTopologyChanged(token.getMaskTopologyTypes()); + // MapTool.getFrame() + // .getCurrentZoneRenderer() + // .getZone() + // .tokenMaskTopologyChanged(token.getMaskTopologyTypes()); return true; } @@ -979,7 +1007,7 @@ private void updateStatesPanel() { groups.put("", noGroupPanel); for (BooleanTokenOverlay overlay : overlays) { String group = overlay.getGroup(); - if (group != null && (group = group.trim()).length() != 0) { + if (group != null && !(group = group.trim()).isEmpty()) { JPanel panel = groups.get(group); if (panel == null) { panel = @@ -1001,7 +1029,7 @@ private void updateStatesPanel() { for (BooleanTokenOverlay state : overlays) { String group = state.getGroup(); var panel = groups.get(""); - if (group != null && (group = group.trim()).length() != 0) { + if (group != null && !(group = group.trim()).isEmpty()) { panel = groups.get(group); } panel.add(new JCheckBox(state.getName())); @@ -1016,7 +1044,7 @@ private void updateStatesPanel() { JPanel barPanel = new JPanel(new MigLayout("wrap 2", "[fill,grow][fill,grow]")); barPanel.setName("bar"); // Add sliders to the bar panel - if (MapTool.getCampaign().getTokenBarsMap().size() > 0) { + if (!MapTool.getCampaign().getTokenBarsMap().isEmpty()) { barPanel.setBorder( BorderFactory.createTitledBorder(I18N.getText("CampaignPropertiesDialog.tab.bars"))); @@ -1181,6 +1209,22 @@ public JCheckBox getAllowURLAccess() { return (JCheckBox) getComponent("@allowURIAccess"); } + public JCheckBox getSnapToGrid() { + return getCheckBox("@isSnapToGrid"); + } + + public JCheckBox getFlippedIso() { + return getCheckBox("@isFlippedIso"); + } + + public JCheckBox getFlippedX() { + return getCheckBox("@isFlippedX"); + } + + public JCheckBox getFlippedY() { + return getCheckBox("@isFlippedY"); + } + public void initSpeechPanel() { getSpeechClearAllButton() .addActionListener( @@ -1239,7 +1283,9 @@ public void initTokenDetails() { } public void initTokenLayoutPanel() { - TokenLayoutPanel layoutPanel = new TokenLayoutPanel(); + TokenLayoutRenderPanel layoutPanel = new TokenLayoutRenderPanel(); + TokenLayoutPanelHelper layoutHelper = + new TokenLayoutPanelHelper(this, layoutPanel, getOKButton()); layoutPanel.setMinimumSize(new Dimension(150, 125)); layoutPanel.setPreferredSize(new Dimension(150, 125)); layoutPanel.setName("tokenLayout"); @@ -1275,8 +1321,8 @@ public ImageAssetPanel getCharSheetPanel() { return (ImageAssetPanel) getComponent("charsheet"); } - public TokenLayoutPanel getTokenLayoutPanel() { - return (TokenLayoutPanel) getComponent("tokenLayout"); + public TokenLayoutRenderPanel getTokenLayoutPanel() { + return (TokenLayoutRenderPanel) getComponent("tokenLayout"); } public TokenTopologyPanel getTokenTopologyPanel() { @@ -1526,7 +1572,7 @@ public void initHeroLabImageList() { if (heroLabData != null) { getTokenIconPanel().setImageId(heroLabData.getImageAssetID(index)); - getTokenLayoutPanel().setTokenImage(heroLabData.getImageAssetID(index)); + getTokenLayoutPanel().getHelper().setTokenImageId(heroLabData.getImageAssetID(index)); } }); @@ -1596,7 +1642,7 @@ public void initStatBlocks() { xmlStatblockRSyntaxTextArea.revalidate(); } catch (IOException e) { - e.printStackTrace(); + log.debug(e.getLocalizedMessage(), e); } RTextScrollPane xmlStatblockRTextScrollPane = new RTextScrollPane(xmlStatblockRSyntaxTextArea); @@ -1618,7 +1664,7 @@ public void initStatBlocks() { textStatblockRSyntaxTextArea.revalidate(); } catch (IOException e) { - e.printStackTrace(); + log.debug(e.getLocalizedMessage(), e); } RTextScrollPane textStatblockRTextScrollPane = @@ -1659,7 +1705,7 @@ public void initStatBlocks() { MD5Key tokenImageKey = heroLabData.getTokenImage(); if (tokenImageKey != null) { getTokenIconPanel().setImageId(tokenImageKey); - getTokenLayoutPanel().setTokenImage(tokenImageKey); + getTokenLayoutPanel().getHelper().setTokenImageId(tokenImageKey); } MD5Key portraitAssetKeY = heroLabData.getPortraitImage(); @@ -1869,7 +1915,7 @@ public Map getMap() { Map map = new HashMap(); for (Association row : rowList) { - if (row.getLeft() == null || row.getLeft().trim().length() == 0) { + if (row.getLeft() == null || row.getLeft().trim().isEmpty()) { continue; } map.put(row.getLeft(), row.getRight()); @@ -1928,7 +1974,7 @@ public MTMultilineStringPopupPanel(String paramString) { j.revalidate(); } catch (IOException e) { - e.printStackTrace(); + log.debug(e.getLocalizedMessage(), e); } JScrollPane localJScrollPane = new RTextScrollPane(j); localJScrollPane.setVerticalScrollBarPolicy(22); @@ -1997,7 +2043,7 @@ private static class WordWrapCellRenderer extends RSyntaxTextArea implements Tab revalidate(); } catch (IOException e) { - e.printStackTrace(); + log.debug(e.getLocalizedMessage(), e); } } @@ -2043,7 +2089,7 @@ protected Void doInBackground() { @Override protected void process(List areaChunk) { if (!isCancelled()) { - final var newArea = areaChunk.get(areaChunk.size() - 1); + final var newArea = areaChunk.getLast(); final var optimizedArea = TokenVBL.simplifyArea( newArea, @@ -2062,7 +2108,7 @@ protected void done() { } } - class SliderListener implements ChangeListener { + class OpacitySliderListener implements ChangeListener { public void stateChanged(ChangeEvent e) { JSlider source = (JSlider) e.getSource(); @@ -2088,13 +2134,13 @@ public Component getListCellRendererComponent( (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); try { ImageIcon finalImage = - ImageUtil.scaleImage( + ImageUtil.scaleImageIcon( new ImageIcon(ImageManager.getImageAndWait(heroLabData.getImageAssetID(index))), 250, 175); label.setIcon(finalImage); } catch (Exception e) { - e.printStackTrace(); + log.debug(e.getLocalizedMessage(), e); } label.setIconTextGap(10); label.setHorizontalTextPosition(JLabel.LEFT); diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java new file mode 100644 index 0000000000..a25bca4966 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java @@ -0,0 +1,1317 @@ +/* + * 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.token.dialog.edit; + +import com.formdev.flatlaf.FlatIconColors; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.extras.FlatSVGIcon; +import com.formdev.flatlaf.extras.components.FlatButton; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyVetoException; +import java.beans.VetoableChangeListener; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.*; +import java.util.function.Function; +import javax.swing.*; +import net.rptools.lib.MD5Key; +import net.rptools.lib.MathUtil; +import net.rptools.lib.image.ImageUtil; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.swing.AbeillePanel; +import net.rptools.maptool.client.swing.GenericDialog; +import net.rptools.maptool.client.swing.SpinnerSliderPaired; +import net.rptools.maptool.client.swing.VerticalLabel; +import net.rptools.maptool.client.ui.theme.Images; +import net.rptools.maptool.client.ui.theme.RessourceManager; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.*; +import net.rptools.maptool.util.GraphicsUtil; +import net.rptools.maptool.util.ImageManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Support class that does much of the heavy lifting for TokenLayoutPanel. Links control components + * and the mouse events to property changes in the reference token. Stores a bunch of useful values. + * Disables the default OK button click on "Enter" when editing spinner fields. + * + * @author 2024 - Reverend/Bubblobill + */ +public class TokenLayoutPanelHelper { + private static final Logger log = LogManager.getLogger(TokenLayoutPanelHelper.class); + + public TokenLayoutPanelHelper( + AbeillePanel parentAbeillePanel, TokenLayoutRenderPanel renderPane, AbstractButton okBtn) { + parent = parentAbeillePanel; + renderPanel = renderPane; + renderPanel.setHelper(this); + setOKButton((JButton) okBtn); + init(); + + parent.addComponentListener( + new ComponentAdapter() { + private void doStuff() { + if (parentRoot == null) { + setParentRoot(parent.getRootPane()); + } + iFeelDirty(); + } + + @Override + public void componentMoved(ComponentEvent e) { + super.componentMoved(e); + doStuff(); + } + + @Override + public void componentShown(ComponentEvent e) { + super.componentShown(e); + doStuff(); + } + + @Override + public void componentResized(ComponentEvent e) { + super.componentResized(e); + doStuff(); + } + }); + } + + public void init() { + getSizeCombo().addItemListener(sizeListener); + initButtons(); + initSpinners(); + initSliders(); + pairControls(); + } + + enum FlipState { + HORIZONTAL, + ISOMETRIC, + VERTICAL + } + + EnumSet flipStates = EnumSet.noneOf(FlipState.class); + private static final UIDefaults UI_DEFAULTS = UIManager.getDefaults(); + private static final double DEFAULT_FONT_SIZE = UI_DEFAULTS.getFont("defaultFont").getSize2D(); + private static final int ICON_SIZE = (int) (DEFAULT_FONT_SIZE * 2 + 4); + + Grid grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); + RenderBits renderBits = new RenderBits(); + boolean isoGrid = grid.isIsometric(); + boolean noGrid = GridFactory.getGridType(grid) == GridFactory.NONE; + boolean squareGrid = GridFactory.getGridType(grid) == GridFactory.SQUARE; + boolean hexGrid = false; + boolean isIsoFigure = false; + int gridSize = grid.getSize(); + double cellHeight = grid.getCellHeight(); + double cellWidth = grid.getCellWidth(); + private static final CellPoint ORIGIN = new CellPoint(0, 0); + private Token token, originalToken, mirrorToken; + private BufferedImage tokenImage; + TokenFootprint footprint; + Rectangle2D footprintBounds; + Set occupiedCells; + ArrayList cellCentres; + // controls/components + AbeillePanel parent; + private JRootPane parentRoot; + private TokenLayoutRenderPanel renderPanel; + private JComboBox sizeCombo; + private JLabel rotationLabel, scaleLabel; + private JSpinner anchorXSpinner, anchorYSpinner, rotationSpinner, scaleSpinner, zoomSpinner; + private JSlider anchorXSlider, anchorYSlider, rotationSlider, scaleSlider, zoomSlider; + private AbstractButton scaleButton, layoutHelpButton, okButton; + + public void setOKButton(JButton b) { + okButton = b; + } + + public void setParentRoot(JRootPane rp) { + parentRoot = rp; + } + + private String helpText = assembleHelpText(); + + // @formatter:off + // spotless:off + // Component Getters + public JComboBox getSizeCombo(){ if (sizeCombo == null) sizeCombo = (JComboBox) parent.getComponent("size"); return sizeCombo; } + // Labels + public JLabel getRotationLabel(){ if (rotationLabel == null) rotationLabel = parent.getLabel("rotationLabel"); return rotationLabel; } + public JLabel getScaleLabel(){ if (scaleLabel == null) scaleLabel = parent.getLabel("scaleLabel"); return scaleLabel; } + // Spinners + public JSpinner getAnchorXSpinner() { if (anchorXSpinner == null) anchorXSpinner = parent.getSpinner("anchorXSpinner"); return anchorXSpinner; } + public JSpinner getAnchorYSpinner() { if (anchorYSpinner == null) anchorYSpinner = parent.getSpinner("anchorYSpinner"); return anchorYSpinner; } + public JSpinner getRotationSpinner() { if (rotationSpinner == null) rotationSpinner = parent.getSpinner("rotationSpinner"); return rotationSpinner; } + public JSpinner getScaleSpinner() { if (scaleSpinner == null) scaleSpinner = parent.getSpinner("scaleSpinner"); return scaleSpinner; } + public JSpinner getZoomSpinner() { if (zoomSpinner == null) zoomSpinner = parent.getSpinner("zoomSpinner"); return zoomSpinner; } + // Sliders + public JSlider getAnchorXSlider() { if (anchorXSlider == null) anchorXSlider = (JSlider) parent.getComponent("anchorXSlider"); return anchorXSlider; } + public JSlider getAnchorYSlider() { if (anchorYSlider == null) anchorYSlider = (JSlider) parent.getComponent("anchorYSlider"); return anchorYSlider; } + public JSlider getRotationSlider() { if (rotationSlider == null) rotationSlider = (JSlider) parent.getComponent("rotationSlider"); return rotationSlider; } + public JSlider getScaleSlider() { if (scaleSlider == null) scaleSlider = (JSlider) parent.getComponent("scaleSlider"); return scaleSlider; } + public JSlider getZoomSlider() { if (zoomSlider == null) zoomSlider = (JSlider) parent.getComponent("zoomSlider"); return zoomSlider; } + // Buttons + public AbstractButton getScaleButton() { if (scaleButton == null) scaleButton = parent.getButton("scaleButton"); return scaleButton; } + public AbstractButton getHelpButton() {if (layoutHelpButton == null) layoutHelpButton = parent.getButton("layoutHelpButton"); return layoutHelpButton; } + // Panel + public TokenLayoutRenderPanel getRenderPanel() { return renderPanel; } + // Linked controls + SpinnerSliderPaired anchorXPair, anchorYPair, rotationPair, scalePair, zoomPair; + + + // Token value getters pointing to the mirror token + public double getTokenSizeScale() { return mirrorToken.getSizeScale(); } + public double getTokenScaleX() { return mirrorToken.getScaleX(); } + public double getTokenScaleY() { return mirrorToken.getScaleY(); } + public double getTokenImageRotation() { return mirrorToken.getImageRotation(); } + public int getTokenAnchorX() { return mirrorToken.getAnchorX(); } + public int getTokenAnchorY() { return mirrorToken.getAnchorY(); } + public boolean getTokenFlippedX() { return mirrorToken.isFlippedX(); } + public boolean getTokenFlippedY() { return mirrorToken.isFlippedY(); } + public boolean getTokenFlippedIso() { return mirrorToken.isFlippedIso(); } + + // Token value setters pointing to the mirror token + protected void setTokenScaleX(double scale) { mirrorToken.setScaleX(scale);} + protected void setTokenScaleY(double scale) { mirrorToken.setScaleY(scale); } + protected void setTokenSizeScale(double scale) { mirrorToken.setSizeScale(scale); } + protected void setTokenAnchorX(Number x) { mirrorToken.setAnchorX(x.intValue()); } + protected void setTokenAnchorY(Number y) { mirrorToken.setAnchorY(y.intValue()); } + protected void setTokenImageRotation(double value) { mirrorToken.setImageRotation(MathUtil.doublePrecision(value, 4)); } + protected void setTokenImageRotation(Number value) { mirrorToken.setImageRotation(MathUtil.doublePrecision(value.doubleValue(), 4)); } + protected void setTokenFlipIso(Boolean b) { + mirrorToken.setFlippedIso(b); + if(flipStates.contains(FlipState.ISOMETRIC) && !b){ + flipStates.remove(FlipState.ISOMETRIC); + } else if(!flipStates.contains(FlipState.ISOMETRIC) && b){ + flipStates.add(FlipState.ISOMETRIC); + } + iFeelDirty(); + } + protected void setTokenFlipX(Boolean b) { + mirrorToken.setFlippedX(b); + if(flipStates.contains(FlipState.HORIZONTAL) && !b){ + flipStates.remove(FlipState.HORIZONTAL); + } else if(!flipStates.contains(FlipState.HORIZONTAL) && b){ + flipStates.add(FlipState.HORIZONTAL); + } + iFeelDirty(); + } + protected void setTokenFlipY(Boolean b) { + mirrorToken.setFlippedY(b); + if(flipStates.contains(FlipState.VERTICAL) && !b){ + flipStates.remove(FlipState.VERTICAL); + } else if(!flipStates.contains(FlipState.VERTICAL) && b){ + flipStates.add(FlipState.VERTICAL); + } + iFeelDirty(); + } + //@formatter:on + //spotless:on + + /** + * There is one spinner/slider for three different scale settings. This sets the value for the + * active axis/axes + * + * @param value the scale value being set + */ + protected void setScaleByAxis(Number n) { + double value = MathUtil.doublePrecision(n.doubleValue(), 4); + log.debug("scaleAxis: " + scaleAxis + " -> " + value); + switch (scaleAxis) { + case 0 -> setTokenSizeScale(value); + case 1 -> setTokenScaleX(value); + case 2 -> setTokenScaleY(value); + } + } + + /** + * There is one spinner/slider for three different scale settings. This returns the value for the + * active axis/axes + * + * @return double value for appropriate scale + */ + protected Number getScaleByAxis() { + if (scaleAxis == 0) { + return getTokenSizeScale(); + } else if (scaleAxis == 1) { + return getTokenScaleX(); + } else { + return getTokenScaleY(); + } + } + + // used to determine which scale is being edited + private int scaleAxis = 0; // 0 is XY, 1 is X, 2 is Y + + /** + * These functions serve to link the scale and zoom sliders and spinners and allow a useful + * representation of values < 100%. Slider ranges from -200 to 0 are for values below 100% and 0 + * to 200 for 100% to 300% + */ + Function percentSliderToSpinner = + i -> + i.intValue() <= 0 + ? MathUtil.mapToRange(((Number) i).doubleValue(), -200.0, 0.0, 0.0, 1.0).doubleValue() + : MathUtil.mapToRange(((Number) i).doubleValue(), 0.0, 200.0, 1.0, 3.0).doubleValue(); + + Function percentSpinnerToSlider = + d -> + d.doubleValue() <= 1 + ? MathUtil.mapToRange(d.doubleValue(), 0.0, 1.0, -200, 0).intValue() + : MathUtil.mapToRange(d.doubleValue(), 1.0, 3.0, 0, 200).intValue(); + + private void storeFlipDirections() { + if (getTokenFlippedIso()) { + flipStates.add(FlipState.ISOMETRIC); + } + if (getTokenFlippedX()) { + flipStates.add(FlipState.HORIZONTAL); + } + if (getTokenFlippedY()) { + flipStates.add(FlipState.VERTICAL); + } + } + + void resetPanel() { + setToken(originalToken, false); + renderPanel.setInitialScale(1d); + renderPanel.calcZoomFactor(); + } + + void resetPanelToDefault() { + setToken(originalToken, true); + } + + // Listeners + PropertyChangeListener controlListener = + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + log.debug("controlListener " + evt); + if (evt.getPropertyName().toLowerCase().contains("spinnervalue")) { + iFeelDirty(); + } else if (evt.getPropertyName().toLowerCase().contains("flip")) { + storeFlipDirections(); + } + } + }; + + PropertyChangeListener mirrorTokenListener = + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + log.debug( + "mirrorTokenListener - " + + evt.getPropertyName() + + ":" + + evt.getOldValue() + + ":" + + evt.getNewValue()); + } + }; + FocusListener focusListener = + new FocusListener() { + // Toggle "Enter" closing the window. + @Override + public void focusGained(FocusEvent e) { + ((JComponent) e.getComponent()).getRootPane().setDefaultButton(null); + } + + @Override + public void focusLost(FocusEvent e) { + ((JComponent) e.getComponent()).getRootPane().setDefaultButton((JButton) okButton); + } + }; + ItemListener sizeListener = + new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (mirrorToken != null) { + setFootprint((TokenFootprint) getSizeCombo().getSelectedItem()); + } + } + }; + + protected Token getMirrorToken() { + return mirrorToken; + } + + /** Mark the rendering panel in need of repainting */ + void iFeelDirty() { + Rectangle panelBounds = getRenderPanel().getBounds(); + panelBounds = getRenderPanel().getBounds(); + RepaintManager.currentManager(getRenderPanel()) + .addDirtyRegion( + getRenderPanel(), panelBounds.x, panelBounds.y, panelBounds.width, panelBounds.height); + } + + public void setTokenImageId(MD5Key tokenImageKey) { + tokenImage = ImageManager.getImage(tokenImageKey); + } + + protected BufferedImage getTokenImage() { + return tokenImage; + } + + public void commitChanges(Token tok) { + tok.setAnchor(getTokenAnchorX(), getTokenAnchorY()); + tok.setSizeScale(getTokenSizeScale()); + tok.setScaleX(getTokenScaleX()); + tok.setScaleY(getTokenScaleY()); + tok.setImageRotation(getTokenImageRotation()); + tok.setFlippedX(flipStates.contains(FlipState.HORIZONTAL)); + tok.setFlippedY(flipStates.contains(FlipState.VERTICAL)); + tok.setFlippedIso(flipStates.contains(FlipState.ISOMETRIC)); + } + + public void setToken(Token token, boolean useDefaults) { + grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); + isoGrid = grid.isIsometric(); + hexGrid = grid.isHex(); + noGrid = GridFactory.getGridType(grid) == GridFactory.NONE; + squareGrid = GridFactory.getGridType(grid) == GridFactory.SQUARE; + renderBits = new RenderBits(); + gridSize = grid.getSize(); + cellHeight = grid.getCellHeight(); + cellWidth = grid.getCellWidth(); + + this.token = token; + this.originalToken = new Token(this.token, true); // duplicate for resetting purposes + // The mirror token exists so we can write token changes without committing them prior to + // clicking OK + this.mirrorToken = new Token(this.token, false); + + if (useDefaults) { + mirrorToken.setImageRotation(0); + mirrorToken.setSizeScale(1d); + mirrorToken.setScaleX(1d); + mirrorToken.setScaleY(1d); + mirrorToken.setAnchor(0, 0); + } + + if (mirrorToken.getFootprint(grid) != (TokenFootprint) getSizeCombo().getSelectedItem()) { + TokenFootprint tmpFP = (TokenFootprint) getSizeCombo().getSelectedItem(); + if (tmpFP != null) { + mirrorToken.setFootprint(grid, grid.getFootprint(tmpFP.getId())); + } + } + + isIsoFigure = + isoGrid && mirrorToken.getShape() == Token.TokenShape.FIGURE && !mirrorToken.isFlippedIso(); + + tokenImage = ImageManager.getImage(mirrorToken.getImageAssetId()); + setFootprint(mirrorToken.getFootprint(grid)); + + getAnchorXSlider().setMinimum((int) -Math.ceil(footprintBounds.getWidth())); + getAnchorXSlider().setMaximum((int) Math.ceil(footprintBounds.getWidth())); + if (isIsoFigure) { + // Allow more vertical travel for iso figures + getAnchorYSlider().setMinimum((int) -Math.ceil(1.4 * footprintBounds.getHeight())); + getAnchorYSlider().setMaximum((int) Math.ceil(1.4 * footprintBounds.getHeight())); + } else { + getAnchorYSlider().setMinimum((int) -Math.ceil(footprintBounds.getHeight())); + getAnchorYSlider().setMaximum((int) Math.ceil(footprintBounds.getHeight())); + } + // align mouse drag bounds with sliders + getRenderPanel().setMaxXoff(getAnchorXSlider().getMaximum()); + getRenderPanel().setMaxYoff(getAnchorYSlider().getMaximum()); + + storeFlipDirections(); + + // Assign Suppliers and Consumers to the linked controls and add a PropertyChangeListener + anchorXPair.setPropertySetter(this::setTokenAnchorX); + anchorXPair.setPropertyGetter(this::getTokenAnchorX); + anchorXPair.setPropertyName("AnchorX"); + anchorXPair.addPropertyChangeListener(controlListener); + + anchorYPair.setPropertySetter(this::setTokenAnchorY); + anchorYPair.setPropertyGetter(this::getTokenAnchorY); + anchorYPair.setPropertyName("AnchorY"); + anchorYPair.addPropertyChangeListener(controlListener); + + rotationPair.setPropertySetter(this::setTokenImageRotation); + rotationPair.setPropertyGetter(this::getTokenImageRotation); + rotationPair.setPropertyName("Rotation"); + rotationPair.addPropertyChangeListener(controlListener); + + scalePair.setPropertySetter(this::setScaleByAxis); + scalePair.setPropertyGetter(this::getScaleByAxis); + scalePair.setPropertyName("Scale"); + scalePair.addPropertyChangeListener(controlListener); + + zoomPair.setPropertySetter(getRenderPanel().getZoomConsumer()); + zoomPair.setPropertyGetter(getRenderPanel().getZoomSupplier()); + zoomPair.setPropertyName("Zoom"); + zoomPair.addPropertyChangeListener(controlListener); + + setControlValues(); + getRenderPanel() + .addComponentListener( + new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + super.componentShown(e); + renderBits.init(); + } + }); + } + + private void setFootprint(TokenFootprint fp) { + this.footprint = fp; + setCentredFootprintBounds(); + occupiedCells = footprint.getOccupiedCells(ORIGIN); + Rectangle2D aggregateBounds = new Rectangle2D.Double(); + cellCentres = new ArrayList<>(occupiedCells.size()); + for (CellPoint cp : occupiedCells) { + cellCentres.add(grid.getCellCenter(cp)); + aggregateBounds.add(grid.getBounds(cp)); + } + double xFix = -aggregateBounds.getCenterX(); + double yFix = -aggregateBounds.getCenterY(); + cellCentres.replaceAll(pt -> pt = new Point2D.Double(pt.getX() + xFix, pt.getY() + yFix)); + } + + private void setCentredFootprintBounds() { + if (grid == null) { + return; + } + if (!noGrid) { + footprintBounds = footprint.getBounds(grid, ORIGIN); + footprintBounds = + new Rectangle2D.Double( + -footprintBounds.getWidth() / 2d, + -footprintBounds.getHeight() / 2d, + footprintBounds.getWidth(), + footprintBounds.getHeight()); + } else { + double factor = footprint.getScale(); + footprintBounds = + new Rectangle2D.Double( + -gridSize / 2d * factor, + -gridSize / 2d * factor, + gridSize * factor, + gridSize * factor); + } + } + + /** Link the spinners to the sliders and join them in matrimonial bliss */ + private void pairControls() { + anchorXPair = new SpinnerSliderPaired(getAnchorXSpinner(), getAnchorXSlider()); + anchorYPair = new SpinnerSliderPaired(getAnchorYSpinner(), getAnchorYSlider()); + + rotationPair = new SpinnerSliderPaired(getRotationSpinner(), getRotationSlider()); + rotationPair.setSpinnerWraps(true); + + scalePair = + new SpinnerSliderPaired( + getScaleSpinner(), + getScaleSlider(), + null, + percentSpinnerToSlider, + percentSliderToSpinner); + scalePair.addVetoableChangeListener( + new VetoableChangeListener() { + @Override + public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { + if (evt.getPropertyName().toLowerCase().contains("value") + && evt.getNewValue().getClass().isAssignableFrom(Double.class) + && ((Number) evt.getNewValue()).doubleValue() < 0.1) { + throw new PropertyVetoException("Minimum scale value reached", evt); + } + } + }); + zoomPair = + new SpinnerSliderPaired( + getZoomSpinner(), + getZoomSlider(), + null, + percentSpinnerToSlider, + percentSliderToSpinner); + zoomPair.addVetoableChangeListener( + new VetoableChangeListener() { + @Override + public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { + if (evt.getPropertyName().toLowerCase().contains("value") + && evt.getNewValue().getClass().isAssignableFrom(Double.class) + && ((Number) evt.getNewValue()).doubleValue() < 0.34) { + throw new PropertyVetoException("Minimum zoom value reached", evt); + } + } + }); + anchorXPair.addPropertyChangeListener(evt -> iFeelDirty()); + anchorYPair.addPropertyChangeListener(evt -> iFeelDirty()); + rotationPair.addPropertyChangeListener(evt -> iFeelDirty()); + scalePair.addPropertyChangeListener(evt -> iFeelDirty()); + zoomPair.addPropertyChangeListener(evt -> iFeelDirty()); + } + + public void initSpinners() { + // models + getAnchorXSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d)); + getAnchorYSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d)); + getRotationSpinner().setModel(new SpinnerNumberModel(0d, 0d, 360d, 1d)); + getScaleSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1)); + getZoomSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1)); + getZoomSpinner().setVisible(false); + // editors + getAnchorXSpinner().setEditor(new JSpinner.NumberEditor(anchorXSpinner, "0")); + getAnchorYSpinner().setEditor(new JSpinner.NumberEditor(anchorYSpinner, "0")); + getRotationSpinner().setEditor(new JSpinner.NumberEditor(rotationSpinner, "0.0")); + getScaleSpinner().setEditor(new JSpinner.NumberEditor(scaleSpinner, "0%")); + + ((JSpinner.NumberEditor) getAnchorXSpinner().getEditor()).getTextField().setColumns(3); + ((JSpinner.NumberEditor) getAnchorYSpinner().getEditor()).getTextField().setColumns(3); + ((JSpinner.NumberEditor) getRotationSpinner().getEditor()).getTextField().setColumns(3); + ((JSpinner.NumberEditor) getScaleSpinner().getEditor()).getTextField().setColumns(4); + + // listeners + ((JSpinner.NumberEditor) getAnchorXSpinner().getEditor()) + .getTextField() + .addFocusListener(focusListener); + ((JSpinner.NumberEditor) getAnchorYSpinner().getEditor()) + .getTextField() + .addFocusListener(focusListener); + ((JSpinner.NumberEditor) getRotationSpinner().getEditor()) + .getTextField() + .addFocusListener(focusListener); + ((JSpinner.NumberEditor) getScaleSpinner().getEditor()) + .getTextField() + .addFocusListener(focusListener); + } + + public void initButtons() { + // icons + createButtonIcons(); + + scaleButton = new FlatButton(); + parent.replaceComponent("scalePanel", "scaleButton", scaleButton); + getScaleButton().addActionListener(new ScaleButtonListener()); + getScaleButton().setIcon(scaleIcons[scaleAxis]); + getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " "); + + layoutHelpButton = new FlatButton(); + ((FlatButton) layoutHelpButton).setButtonType(FlatButton.ButtonType.help); + parent.replaceComponent("layoutTabPanel", "layoutHelpButton", layoutHelpButton); + layoutHelpButton.setToolTipText(helpText); + layoutHelpButton.addActionListener(e -> showHelp()); + layoutHelpButton.addMouseListener( + new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + super.mouseExited(e); + iFeelDirty(); + } + }); + } + + private String assembleHelpText() { + String rowText = "%s%s"; + String caption = ""; + StringBuilder sb = new StringBuilder(); + sb.append(caption.formatted(I18N.getString("EditTokenDialog.layout.help.caption"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.leftDrag"), + I18N.getString("EditTokenDialog.layout.help.moveImage"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.rightDrag"), + I18N.getString("EditTokenDialog.layout.help.moveView"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.leftDoubleClick"), + I18N.getString("EditTokenDialog.layout.help.reset"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.rightDoubleClick"), + I18N.getString("EditTokenDialog.layout.help.resetDefaults"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.wheel"), + I18N.getString("EditTokenDialog.layout.help.scaleImage"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.shiftWheel"), + I18N.getString("EditTokenDialog.layout.help.rotateImage"))); + sb.append( + rowText.formatted( + I18N.getString("Mouse.ctrlWheel"), + I18N.getString("EditTokenDialog.layout.help.zoomView"))); + return sb.toString(); + } + + private void showHelp() { + JPanel helpPanel = new JPanel(new BorderLayout()); + GenericDialog gd = new GenericDialog("Layout Controls", MapTool.getFrame(), helpPanel, true); + gd.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + JButton okayButton = new JButton(); + okayButton.setText(I18N.getString("Button.ok")); + + JLabel helpTextContainer = new JLabel(); + helpTextContainer.setText(helpText); + helpPanel.add(new JScrollPane(helpTextContainer), BorderLayout.NORTH); + helpPanel.add(okayButton, BorderLayout.SOUTH); + okayButton.addActionListener( + e -> { + gd.closeDialog(); + iFeelDirty(); + }); + gd.showDialog(); + } + + public void initSliders() { + getAnchorXSlider().setModel(new DefaultBoundedRangeModel(0, gridSize, -gridSize, gridSize)); + getAnchorYSlider().setModel(new DefaultBoundedRangeModel(0, gridSize, -gridSize, gridSize)); + getScaleSlider().setModel(new DefaultBoundedRangeModel(0, 0, -200, 200)); + getZoomSlider().setModel(new DefaultBoundedRangeModel(0, 0, -200, 200)); + getRotationSlider().setModel(new DefaultBoundedRangeModel(0, 360, 0, 360)); + + class VertLabel extends VerticalLabel { + public double divisor = 1; + + public VertLabel(String text) { + super(text); + } + + public void setDivisor(double divisor) { + this.divisor = divisor; + } + + protected void paintComponent(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + if (isRotated()) + g2d.rotate( + Math.toRadians(-90 * getRotation() / divisor), + getPreferredSize().getWidth(), + getPreferredSize().getHeight()); + + super.paintComponent(g2d); + g2d.dispose(); + } + } + Dictionary offSetYLabels = new Hashtable(); + Dictionary offSetXLabels = new Hashtable(); + for (int i = -1200; i <= 1200; i += 50) { + JLabel label1 = new JLabel(String.valueOf(i)); + offSetYLabels.put(i, label1); + VertLabel label2 = new VertLabel(String.valueOf(i)); + label2.setRotation(VerticalLabel.ROTATE_RIGHT); + label2.setDivisor(3d); + offSetXLabels.put(i, label2); + } + getAnchorYSlider().setLabelTable(offSetYLabels); + getAnchorXSlider().setLabelTable(offSetXLabels); + + DecimalFormat df = new DecimalFormat("##0%"); + Dictionary pctLabels = new Hashtable(); + pctLabels.put(-200, new JLabel(df.format(0))); + pctLabels.put(-100, new JLabel(df.format(0.5))); + pctLabels.put(0, new JLabel(df.format(1))); + pctLabels.put(100, new JLabel(df.format(2))); + pctLabels.put(200, new JLabel(df.format(3))); + + getScaleSlider().setLabelTable(pctLabels); + getZoomSlider().setLabelTable(pctLabels); + + setupSlider(getAnchorXSlider(), 50, 25); + setupSlider(getAnchorYSlider(), 50, 25); + setupSlider(getRotationSlider(), 60, 12); + setupSlider(getScaleSlider(), 100, 20); + setupSlider(getZoomSlider(), 100, 20); + } + + private void setupSlider(JSlider slider, int majorTick, int minorTick) { + slider.setMajorTickSpacing(majorTick); + slider.setMinorTickSpacing(minorTick); + slider.setPaintTicks(true); + slider.setPaintLabels(true); + } + + /** Set controls to match token values */ + private void setControlValues() { + getAnchorXSpinner().setValue(getTokenAnchorX()); + getAnchorYSpinner().setValue(getTokenAnchorY()); + scaleAxis = 0; + getScaleButton().setIcon(scaleIcons[scaleAxis]); + getScaleSpinner().setValue(getTokenSizeScale()); + getRotationSpinner().setValue(getTokenImageRotation()); + getZoomSpinner().setValue(1d); + } + + String scaleStartSVG = + ""; + String scaleNSSVG = + ""; + String scaleEWSVG = + ""; + String scaleEndSVG = ""; + + public static ImageIcon makeSVGIcon(int size, String svg) { + InputStream stream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); + FlatSVGIcon.ColorFilter colorFilter = new FlatSVGIcon.ColorFilter(); + // Convert a the original color into another one supposedly + colorFilter.add(Color.BLACK, UI_DEFAULTS.getColor(FlatIconColors.OBJECTS_BLACK_TEXT)); + colorFilter.add(Color.WHITE, UI_DEFAULTS.getColor(FlatIconColors.ACTIONS_BLUE_DARK)); + try { + FlatSVGIcon flatSVGIcon = new FlatSVGIcon(stream); + flatSVGIcon.setColorFilter(colorFilter); + return flatSVGIcon.derive(size, size); + } catch (IOException e) { + return null; + } + } + + ImageIcon[] scaleIcons = new ImageIcon[3]; + + private void createButtonIcons() { + scaleIcons[0] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleNSSVG + scaleEWSVG + scaleEndSVG); + scaleIcons[1] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleEWSVG + scaleEndSVG); + scaleIcons[2] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleNSSVG + scaleEndSVG); + } + + private class ScaleButtonListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + scaleAxis = scaleAxis == 2 ? 0 : scaleAxis + 1; + getScaleButton().setIcon(scaleIcons[scaleAxis]); + switch (scaleAxis) { + case 0 -> { + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenSizeScale()).intValue()); + getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " "); + } + case 1 -> { + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleX()).intValue()); + getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-X"); + } + case 2 -> { + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleY()).intValue()); + getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-Y"); + } + } + } + } + + /** + * Just a convenient place to hold a bunch of stuff that is not token related but purely for the + * rendering panel + */ + class RenderBits { + RenderBits() { + if (FlatLaf.isLafDark()) { + panelTexture = ImageUtil.negativeImage(panelTexture); + for (int i = 0; i < colours.length; i++) { + colours[i] = new Color(ImageUtil.negativeColourInt(colours[i].getRGB())); + } + } + + backgroundTexture = + new TexturePaint( + panelTexture, new Rectangle(0, 0, panelTexture.getWidth(), panelTexture.getHeight())); + setStrokeArrays(); + gridShapeFill = createGridShape(false); + gridShapeOutline = createGridShape(true); + } + + void init() { + Dimension size = getRenderPanel().getSize(); + viewBounds = getRenderPanel().getVisibleRect(); + viewOffset = getRenderPanel().getViewOffset(); + centrePoint = + new Point2D.Double( + size.width / 2 + viewOffset.getX(), size.height / 2 + viewOffset.getY()); + + if (zoomFactor != getRenderPanel().getZoomFactor()) { + zoomFactor = getRenderPanel().getZoomFactor(); + constrainedZoom = MathUtil.constrainNumber(zoomFactor, 1, 1.6d).floatValue(); + setStrokeArrays(); + } + + if (tokenImage == null) { + // just to avoid Div/0 if called before image loaded + tokenImage = + new BufferedImage( + (int) footprintBounds.getWidth(), + (int) footprintBounds.getHeight(), + BufferedImage.TYPE_4BYTE_ABGR_PRE); + } + + if (isIsoFigure) { + // If figure we need to calculate an additional offset for the token height outside + // footprint + double imageFitRatio = + footprintBounds.getWidth() / tokenImage.getWidth(); // scale for width + if (tokenImage.getHeight() * footprintBounds.getWidth() / tokenImage.getWidth() + > 2 * footprintBounds.getHeight()) { + // if this results in being more than twice the footprint height, use height instead + imageFitRatio = footprintBounds.getHeight() * 2 / tokenImage.getHeight(); + } + double th = tokenImage.getHeight() * imageFitRatio; + iso_figure_ho = (footprintBounds.getHeight() - th); + } + + workImage = ImageUtil.getScaledTokenImage(tokenImage, mirrorToken, grid, zoomFactor); + workImage = getFlippedImage(workImage); + workImage = ImageUtil.rotateImage(workImage, mirrorToken.getImageRotation()); + } + + private void setStrokeArrays() { + if (solidStrokes == null) { + solidStrokes = new BasicStroke[strokeModels.length]; + dashedStrokes = new BasicStroke[strokeModels.length]; + } + float thickest = strokeModels[0].getLineWidth(); + float thinnest = strokeModels[3].getLineWidth(); + boolean mapValues = constrainedZoom != 1f; + for (int i = 0; i < strokeModels.length; i++) { + BasicStroke model = strokeModels[i]; + float useWidth = 0; + if (mapValues) { + useWidth = + MathUtil.mapToRange( + model.getLineWidth(), + thinnest, + thickest, + thinnest * constrainedZoom, + thickest * constrainedZoom); + solidStrokes[i] = new BasicStroke(useWidth, model.getEndCap(), model.getLineJoin()); + dashedStrokes[i] = + new BasicStroke( + useWidth, + model.getEndCap(), + model.getLineJoin(), + model.getMiterLimit(), + model.getDashArray(), + model.getDashPhase()); + } + } + } + + static final RenderingHints RENDERING_HINTS = ImageUtil.getRenderingHintsQuality(); + static final float LINE_SIZE = (float) (DEFAULT_FONT_SIZE / 12f); + Rectangle2D viewBounds; + Point2D viewOffset, centrePoint; + double zoomFactor = 1, iso_figure_ho = 0; + float constrainedZoom = 1; + private static BufferedImage panelTexture = RessourceManager.getImage(Images.TEXTURE_PANEL); + BufferedImage workImage; + static TexturePaint backgroundTexture; + Shape centreMark, gridShapeFill, gridShapeOutline; + BasicStroke[] solidStrokes, dashedStrokes; + BasicStroke[] strokeModels = + new BasicStroke[] { + new BasicStroke( + LINE_SIZE * 2.35f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 1f, + new float[] {4f, 6f}, + 2f), + new BasicStroke( + LINE_SIZE * 1.63f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 1f, + new float[] {3.5f, 6.5f}, + 1.5f), + new BasicStroke( + LINE_SIZE * 1.45f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 1f, + new float[] {2f, 8f}, + 1.5f), + new BasicStroke( + LINE_SIZE, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 1f, + new float[] {2.5f, 7.5f}, + 1.5f) + }; + public Color[] colours = + new Color[] {Color.YELLOW, Color.RED, Color.BLUE, Color.BLACK, Color.DARK_GRAY}; + + /** + * Returns an image flipped according to the token's flip properties + * + * @param bi Image to flip + * @return Flipped bufferedImage + */ + protected BufferedImage getFlippedImage(BufferedImage bi) { + log.debug("getFlippedImage - flipStates: " + flipStates); + int direction = + 0 + + (flipStates.contains(FlipState.HORIZONTAL) ? 1 : 0) + + (flipStates.contains(FlipState.VERTICAL) ? 2 : 0); + if (direction != 0) { + bi = ImageUtil.flipCartesian(bi, direction); + } + if (flipStates.contains(FlipState.ISOMETRIC)) { + bi = ImageUtil.flipIsometric(bi, true); + } + return bi; + } + + /** + * Rotates a buffered image by the provided angle in degrees + * + * @param bi Image to transform + * @param angle Angle in degrees + * @return Rotated bufferedImage + */ + protected BufferedImage getRotatedImage(BufferedImage bi, Number angle) { + return ImageUtil.rotateImage(bi, angle.doubleValue()); + } + + private Shape createGridShape(boolean trueSize) { + return GraphicsUtil.createGridShape( + GridFactory.getGridType(grid), (trueSize ? grid.getSize() : grid.getSize() - 8)); + } + + /** Itty bitty cross to show the dead-centre of the footprint */ + void createCentreMark() { + double aperture = Math.max(cellHeight, cellWidth) / 7.0; + double r = aperture / 4.0; + Path2D path = new Path2D.Double(); + path.moveTo(-r, -r); + path.lineTo(r, r); + path.moveTo(-r, r); + path.lineTo(r, -r); + centreMark = path; + } + + protected void paintCentreMark(Graphics g) { + if (centreMark == null) { + createCentreMark(); + } + Graphics2D g2d = (Graphics2D) g; + g2d.translate(centrePoint.getX(), centrePoint.getY()); + g2d.scale(constrainedZoom, constrainedZoom); + g2d.setStroke( + new BasicStroke( + constrainedZoom * 2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 10f)); + g2d.setColor(colours[4]); + Composite oldAc = g2d.getComposite(); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[0])); + g2d.draw(centreMark); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[2])); + g2d.setStroke( + new BasicStroke( + constrainedZoom * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 10f)); + g2d.setColor(colours[3]); + g2d.draw(centreMark); + g2d.setComposite(oldAc); + renderBits.paintShapeOutLine(g2d, centreMark, true, true); + g2d.dispose(); + } + + /** + * Used for gridless maps, paints concentric rings with an interval of the grid size. Paints a + * ring of a different colour at the radius associated with the footprint scale. Also paints + * some radial lines to assist with alignment + * + * @param g + * @param zoomFactor + */ + void paintRings(Graphics g, double zoomFactor) { + Graphics2D g2d = (Graphics2D) g.create(); + // used with no grid. A set of rings and radial lines. + TokenFootprint fp = mirrorToken.getFootprint(grid); + Rectangle2D fpCellBounds = fp.getBounds(grid, ORIGIN); + fpCellBounds = + new Rectangle2D.Double( + 0, + 0, + fpCellBounds.getWidth() * footprint.getScale(), + fpCellBounds.getHeight() * footprint.getScale()); + + double cx = viewBounds.getCenterX() + viewOffset.getX(); + double cy = viewBounds.getCenterY() + viewOffset.getY(); + + double gap = grid.getSize() * zoomFactor; + double maxRadius = Math.hypot(this.viewBounds.getWidth(), this.viewBounds.getHeight()); + double currentRadius = gap / 2d; + double tokenRadius = fpCellBounds.getCenterX() * zoomFactor; + Line2D lineLong = new Line2D.Double(cx + currentRadius / 2d, cy, maxRadius, cy); + Line2D lineShort = new Line2D.Double(cx + gap * 2d, cy, maxRadius, cy); + + // draw radial lines + for (double i = 0; i < 24; i++) { + if (i % 6 == 0) { + continue; // skip cardinal lines + } + paintShapeOutLine( + g2d, + AffineTransform.getRotateInstance(Math.TAU / 24 * i, cx, cy) + .createTransformedShape(i % 2 == 1 ? lineShort : lineLong), + false, + true); + } + + // draw rings + while (currentRadius < maxRadius) { + Ellipse2D e = + new Ellipse2D.Double( + cx - currentRadius, cy - currentRadius, 2 * currentRadius, 2 * currentRadius); + paintShapeOutLine(g2d, e, true, true); + currentRadius += gap; + } + paintShapeOutLine( + g2d, + new Ellipse2D.Double( + cx - tokenRadius, cy - tokenRadius, tokenRadius * 2, tokenRadius * 2), + true, + false); + g2d.dispose(); + } + + /** + * Horizontal and vertical lines oriented on cx/cy + * + * @param g + * @param rotation used to draw lines on rotated images + * @param cx vertical line position + * @param cy horizontal line position + * @param solid Draw as solid else dashed + * @param colourSet1 Use colour set 1 else 2 + */ + void paintCentreLines(Graphics g, double rotation, boolean solid, boolean colourSet1) { + Graphics old = g; + // create crosshair with a central gap + double cx = centrePoint.getX(); + double cy = centrePoint.getY(); + Rectangle2D r = viewBounds; + double x = r.getX() - 1, + y = r.getY() - 1, + w = x + r.getWidth() + 2, + h = y + r.getHeight() + 2; + + double aperture = Math.max(cellHeight, cellWidth) / 5.0; + Path2D lines = new Path2D.Double(); + lines.moveTo(cx, y - h); + lines.lineTo(cx, cy - aperture); + lines.moveTo(cx, cy + aperture); + lines.lineTo(cx, h * 2); + lines.moveTo(x - w, cy); + lines.lineTo(cx - aperture, cy); + lines.moveTo(cx + aperture, cy); + lines.lineTo(2 * w, cy); + Shape s; + if (!MathUtil.inTolerance(rotation, 0, 0.009)) { + s = + AffineTransform.getRotateInstance(Math.toRadians(rotation), cx, cy) + .createTransformedShape(lines); + s = + AffineTransform.getTranslateInstance( + getTokenAnchorX() * zoomFactor, getTokenAnchorY() * zoomFactor) + .createTransformedShape(s); + } else { + s = lines; + } + paintShapeOutLine(g, s, solid, colourSet1); + g = old; + } + + /* + Each line is drawn as a sequence of overlapping lines. + The following arrays are used to define each stroke + */ + float[] opacities = new float[] {0.15f, 0.85f, 0.6f, 1f}; + float[] strokeWidths = + new float[] {2f * LINE_SIZE, 1.5f * LINE_SIZE, LINE_SIZE, 0.5f * LINE_SIZE}; + float[] dashPhases = + new float[] { + 2f * LINE_SIZE + 2f, 2f * LINE_SIZE + 1.75f, 2f * LINE_SIZE + 1f, 2f * LINE_SIZE + 1.25f + }; + float[][] dashes = + new float[][] { + {4f * LINE_SIZE + 4f, 6f}, + {4f * LINE_SIZE + 3.5f, 6.5f}, + {4f * LINE_SIZE + 2f, 8f}, + {4f * LINE_SIZE + 2.5f, 7.5f} + }; + + void paintShapeOutLine(Graphics g, Shape shp, boolean solid, boolean colourSet1) { + Graphics2D g2d = (Graphics2D) g.create(); + Composite oldAc = g2d.getComposite(); + AlphaComposite ac; + BasicStroke[] strokes = solid ? solidStrokes : dashedStrokes; + + for (int i = 0; i < 4; i++) { + switch (i) { + case 0, 1 -> g2d.setColor(colourSet1 ? colours[2] : colours[3]); + case 2, 3 -> g2d.setColor(colourSet1 ? colours[0] : colours[1]); + } + g2d.setStroke( + solid + ? new BasicStroke(strokeWidths[i]) + : new BasicStroke( + strokeWidths[i], + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 1f, + dashes[i], + dashPhases[i])); + + ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[i]); + g2d.setComposite(ac); + g2d.draw(shp); + } + g2d.setComposite(oldAc); + g2d.dispose(); + } + + void paintFootprint(Graphics g, double zoomFactor) { + if (noGrid) { + paintRings(g, zoomFactor); + return; + } + + Graphics2D g2d = (Graphics2D) g.create(); + Shape oldClip = g2d.getClip(); + AffineTransform oldXform = g2d.getTransform(); + g2d.setRenderingHints(RENDERING_HINTS); + + g2d.translate(centrePoint.getX(), centrePoint.getY()); + g2d.scale(zoomFactor, zoomFactor); + + Shape tmpShape; + + Area clipArea = new Area(g2d.getClipBounds()); + Area tmpClip = new Area(), fpArea = new Area(); + g2d.setStroke(new BasicStroke(1f)); + g2d.setColor(colours[4]); + + double footprintScale = footprint.getScale(); + double yCorrection = isIsoFigure ? footprintScale * footprintBounds.getHeight() / 2d : 0; + + Shape scaledOutline, scaledFill; + // for drawing sub-cell-sizes + if (footprintScale < 1d) { + scaledOutline = + AffineTransform.getScaleInstance(footprintScale, footprintScale) + .createTransformedShape(gridShapeOutline); + scaledFill = + AffineTransform.getScaleInstance(footprintScale, footprintScale) + .createTransformedShape(gridShapeFill); + } else { + scaledOutline = gridShapeOutline; + scaledFill = gridShapeFill; + } + for (Point2D pt : cellCentres) { + AffineTransform ptXform = + AffineTransform.getTranslateInstance(pt.getX(), pt.getY() + yCorrection); + if (footprintScale < 1) { + g2d.draw(ptXform.createTransformedShape(gridShapeOutline)); + } + g2d.draw(ptXform.createTransformedShape(scaledOutline)); + tmpShape = ptXform.createTransformedShape(scaledFill); + fpArea.add(new Area(tmpShape)); + } + tmpClip.subtract(fpArea); + g2d.setClip(tmpClip); + g2d.setPaint(FlatLaf.isLafDark() ? new Color(1f, 1f, 1f, 0.35f) : new Color(0, 0, 0, 0.35f)); + g2d.setClip(fpArea); + g2d.fill(fpArea); + + g2d.setClip(clipArea); + paintShapeOutLine(g2d, fpArea, true, true); + g2d.setTransform(oldXform); + g2d.setClip(oldClip); + + g2d.dispose(); + paintExtraGuides(g); + } + + void paintExtraGuides(Graphics g) { + if (noGrid) { + return; + } + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHints(RENDERING_HINTS); + + g2d.translate( + centrePoint.getX(), + centrePoint.getY() + (isIsoFigure ? footprintBounds.getHeight() * zoomFactor / 2d : 0)); + + g2d.setPaint(colours[4]); + g2d.setStroke( + new BasicStroke( + 0.5f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 10f, + new float[] {0.5f, 1.75f}, + 0)); + + double limit = Math.hypot(g2d.getClipBounds().getWidth(), g2d.getClipBounds().getHeight()); + double radius = footprintBounds.getHeight() * zoomFactor; + Rectangle2D bounds = gridShapeOutline.getBounds2D(); + while (radius < limit) { + radius += 2 * cellHeight * zoomFactor; + Shape s = + AffineTransform.getScaleInstance( + radius / bounds.getHeight(), radius / bounds.getHeight()) + .createTransformedShape(gridShapeOutline); + g2d.draw(s); + } + g2d.dispose(); + } + + void paintToken(Graphics g, boolean translucent) { + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHints(RENDERING_HINTS); + AffineTransform oldXform = g2d.getTransform(); + if (centreMark == null) { + createCentreMark(); + } + + g2d.translate(centrePoint.getX(), centrePoint.getY()); + g2d.translate(getTokenAnchorX() * zoomFactor, getTokenAnchorY() * zoomFactor); + // g2d.scale(zoomFactor, zoomFactor); + + Composite oldAc = g2d.getComposite(); + + AffineTransform imageXform = + AffineTransform.getTranslateInstance( + -workImage.getWidth() / 2d, -workImage.getHeight() / 2d); + + if (translucent) { + AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f); + g2d.setComposite(ac); + g2d.drawImage(workImage, imageXform, null); + g2d.setComposite(oldAc); + } else { + g2d.drawImage(workImage, imageXform, null); + } + g2d.dispose(); + double rotAngle = getTokenImageRotation(); + if (rotAngle > 0.01 && rotAngle < 359.99) { + paintCentreLines(g, rotAngle, false, true); + } + } + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java new file mode 100644 index 0000000000..e7c2a6e880 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java @@ -0,0 +1,351 @@ +/* + * 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.token.dialog.edit; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.swing.*; +import net.rptools.lib.MathUtil; +import net.rptools.maptool.client.AppStyle; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.swing.SwingUtil; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.Token; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Support class used by the token editor dialog on the "Properties" tab to allow a token's image to + * be moved around within a one-cell grid area. Scaling is supported using the mousewheel and + * position is supported using left-drag. We should add rotation ability using Shift-mousewheel as + * well. + * + * @author trevor + */ +public class TokenLayoutRenderPanel extends JPanel { + private static final Logger log = LogManager.getLogger(TokenLayoutRenderPanel.class); + + public TokenLayoutRenderPanel() { + evtTarget = MouseTarget.NONE; + addRenderPaneListeners(); + setFocusable(true); + addComponentListener( + new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + super.componentShown(e); + size = getSize(); + requestFocus(); + } + + @Override + public void componentResized(ComponentEvent e) { + super.componentResized(e); + size = getSize(); + zoomFactor = -1; + calcZoomFactor(); + } + }); + log.debug("New TokenLayoutPanel"); + } + + private TokenLayoutPanelHelper helper; + + protected void setHelper(TokenLayoutPanelHelper h) { + helper = h; + } + + public TokenLayoutPanelHelper getHelper() { + return helper; + } + + private enum MouseTarget { + NONE, + IMAGE_OFFSET, + ROTATION, + SCALE, + VIEW_OFFSET, + ZOOM + } + + private MouseTarget evtTarget; + private Token token; + private int viewOffsetX = 0, viewOffsetY = 0, dragStartX, dragStartY; + private int maxXoff = 100, maxYoff = 100; + + public void setMaxXoff(int maxX) { + maxXoff = maxX; + } + + public void setMaxYoff(int maxY) { + maxYoff = maxY; + } + + private double zoomFactor = -1.0; + + public void setInitialScale(double initialScale) { + this.initialScale = initialScale; + } + + private double initialScale = 1.0; + public Supplier zoom = () -> zoomFactor; + public Consumer zoomSet = d -> zoomFactor = MathUtil.doublePrecision(d.doubleValue(), 4); + + Supplier getZoomSupplier() { + return zoom; + } + + Consumer getZoomConsumer() { + return zoomSet; + } + + public double getZoomFactor() { + return zoom.get().doubleValue(); + } + + private void setZoomFactor(double d) { + d = MathUtil.doublePrecision(d, 4); + if (d == zoomFactor) { + return; + } + if (helper.zoomPair.getSpinnerValue() != zoomFactor) { + helper.zoomPair.setValue(d); + } + } + + Dimension size = getSize(); + + protected Point2D.Double getViewOffset() { + return new Point2D.Double(viewOffsetX, viewOffsetY); + } + + public void setToken(Token token) { + log.debug("Setting token"); + zoomFactor = -1d; + this.token = token; + helper.setToken(token, false); + setInitialScale(helper.getTokenSizeScale()); + calcZoomFactor(); + } + + public void reset(Token token) { + setToken(token); + } + + /** Work out a zoom factor to fit the token on screen with a half cell border */ + protected void calcZoomFactor() { + Rectangle2D fpBounds = helper.footprintBounds; + if (token == null || getSize().height == 0 || size == null || fpBounds == null) { + return; + } + fpBounds.setRect( + fpBounds.getWidth() / 2d, + fpBounds.getHeight() / 2d, + fpBounds.getWidth(), + fpBounds.getHeight()); + if (helper.getTokenAnchorX() != 0 || helper.getTokenAnchorY() != 0) { + fpBounds.add( + new Rectangle2D.Double( + fpBounds.getX() + helper.getTokenAnchorX(), + fpBounds.getY() + helper.getTokenAnchorY(), + fpBounds.getWidth() + helper.getTokenAnchorX(), + fpBounds.getHeight() + helper.getTokenAnchorX())); + } + if (getHelper().isIsoFigure) { + double th = token.getHeight() * fpBounds.getWidth() / token.getWidth(); + double iso_ho = fpBounds.getHeight() - th; + fpBounds.add( + new Rectangle2D.Double( + fpBounds.getX(), fpBounds.getY() - iso_ho, fpBounds.getWidth(), th)); + } + double fitWidth = fpBounds.getWidth() + helper.grid.getCellWidth() / 2; + double fitHeight = fpBounds.getHeight() + helper.grid.getCellHeight() / 2; + // which axis has the least space to grow + boolean scaleToWidth = size.getWidth() - fitWidth < size.getHeight() - fitHeight; + // set the zoomfactor + double newZoom = scaleToWidth ? size.getWidth() / fitWidth : size.getHeight() / fitHeight; + + setZoomFactor(newZoom); + log.debug( + "calculated ZoomFactor: " + + zoomFactor + + "\nSize: " + + size + + "\nFootprint bounds: " + + fpBounds); + } + + private void addRenderPaneListeners() { + log.debug("addRenderPaneListeners"); + addMouseListener( + new MouseAdapter() { + String old; + + @Override + public void mouseReleased(MouseEvent e) { + super.mouseReleased(e); + dragStartX = -1; + dragStartY = -1; + evtTarget = MouseTarget.NONE; + helper.iFeelDirty(); + } + + @Override + public void mousePressed(MouseEvent e) { + dragStartX = e.getX(); + dragStartY = e.getY(); + if (SwingUtilities.isLeftMouseButton(e)) { + // start token drag + evtTarget = MouseTarget.IMAGE_OFFSET; + } else if (SwingUtilities.isRightMouseButton(e)) { + // start view drag + evtTarget = MouseTarget.VIEW_OFFSET; + } + } + + @Override + public void mouseEntered(MouseEvent e) { + old = MapTool.getFrame().getStatusMessage(); + MapTool.getFrame() + .setStatusMessage(I18N.getString("EditTokenDialog.status.layout.instructions")); + } + + @Override + public void mouseExited(MouseEvent e) { + if (old != null) MapTool.getFrame().setStatusMessage(old); + evtTarget = MouseTarget.NONE; + helper.iFeelDirty(); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { + viewOffsetX = 0; + viewOffsetY = 0; + maxXoff = 100; + maxYoff = 100; + helper.resetPanel(); + evtTarget = MouseTarget.NONE; + } + if (SwingUtilities.isRightMouseButton(e) && e.getClickCount() == 2) { + viewOffsetX = 0; + viewOffsetY = 0; + maxXoff = 100; + maxYoff = 100; + helper.resetPanelToDefault(); + evtTarget = MouseTarget.NONE; + } + } + }); + addMouseWheelListener( + new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + double delta = e.getPreciseWheelRotation(); + if (delta == 0) { + return; + } + evtTarget = + SwingUtil.isShiftDown(e) + ? MouseTarget.ROTATION + : SwingUtil.isControlDown(e) ? MouseTarget.ZOOM : MouseTarget.SCALE; + + switch (evtTarget) { + case MouseTarget.ROTATION -> { + helper.rotationPair.incrementValue(delta); + } + case MouseTarget.ZOOM -> { + helper.zoomPair.incrementValue(delta); + setZoomFactor(helper.zoomPair.getSpinnerValue()); + } + case MouseTarget.SCALE -> { + // Only for NOT snap-to-scale + if (!token.isSnapToScale()) { + return; + } + helper.scalePair.incrementValue(delta / 16d); + } + default -> log.debug("Defaulting - invalid mouse event target."); + } + evtTarget = MouseTarget.NONE; + helper.iFeelDirty(); + } + }); + addMouseMotionListener( + new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + int dx = e.getX() - dragStartX; + int dy = e.getY() - dragStartY; + if (evtTarget == MouseTarget.IMAGE_OFFSET) { + int offX = MathUtil.constrainInt(helper.getTokenAnchorX() + dx, -maxXoff, maxXoff); + int offY = MathUtil.constrainInt(helper.getTokenAnchorY() + dy, -maxYoff, maxYoff); + helper.anchorXPair.setValue(offX); + helper.anchorYPair.setValue(offY); + + } else if (evtTarget == MouseTarget.VIEW_OFFSET) { + // drag view + viewOffsetX += dx; + viewOffsetY += dy; + repaint(); + } + dragStartX = e.getX(); + dragStartY = e.getY(); + } + }); + } + + @Override + protected void paintBorder(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + AppStyle.shadowBorder.paintWithin(g2d, 0, 0, getSize().width, getSize().height); + super.paintBorder(g); + } + + @Override + protected void paintComponent(Graphics g) { + helper.renderBits.init(); + if (helper.renderBits.viewBounds == null) { + return; + } + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHints(TokenLayoutPanelHelper.RenderBits.RENDERING_HINTS); + // Cleanup + g2d.clearRect(0, 0, size.width, size.height); + // Background + g2d.setPaint(TokenLayoutPanelHelper.RenderBits.backgroundTexture); + g2d.fillRect(0, 0, size.width, size.height); + + if (helper.getTokenImage() == null || size.width == 0) { + return; + } + if (zoomFactor == -1) { + calcZoomFactor(); + return; + } + // Footprint + helper.renderBits.paintFootprint(g, zoomFactor); + // Add horizontal and vertical lines to help with centering + helper.renderBits.paintCentreLines(g, 0, true, false); + // Token + helper.renderBits.paintToken(g, evtTarget.equals(MouseTarget.IMAGE_OFFSET)); + helper.renderBits.paintCentreMark(g); + g2d.dispose(); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form index 57752636be..a28d3ff1a7 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form @@ -1,16 +1,16 @@
- + - + - + @@ -19,7 +19,7 @@ - + @@ -27,6 +27,7 @@ + @@ -38,7 +39,7 @@ - + @@ -550,429 +551,849 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + - - + + + + + + + + + + + + + + + + + + + - + - - + - - + + + + + + + + + + + + + + + + + + - + - - - - - - - + + - + - - - - - - - - - - + + - + - - - - - - - - + + - + - - - - - + + - + - - - - - - + + - + - - - - - - - + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - - - - - - - - + + - + - + - - + + + - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - - + + - + - + - - - - - + - + - + + + - - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - + + - + - - - - - + + + - + - - - - - - + + + + + + + + + + + + + + + + + + - + - - + - - + + + + + + + + + + + + + + + + + + + + + - + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - + + - + - + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + + - + - + - - - - - - - + + - - - + - - - - - - - - - - - - - - - - + + - + - + @@ -1358,7 +1779,7 @@ - + @@ -1457,7 +1878,7 @@ - + @@ -1487,11 +1908,6 @@ - - - - - diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java index 30bee1f16a..d2a0dc7bb7 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java @@ -23,11 +23,11 @@ public class TokenPropertiesDialog { private JPanel headPanel; private JPanel buttonPanel; private JLabel tokenImage; - private JTabbedPane tabPanel; private HtmlEditorSplit gmNotesEditor; private HtmlEditorSplit playerNotesEditor; private JComboBox comboBox1; private JComboBox comboBox2; + private JTabbedPane tabPanel; private JLabel ownershipList; public JComponent getRootComponent() { 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 52a365d108..3f80194786 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 @@ -1444,7 +1444,7 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set wig.dispose(); } // on the iso plane - if (token.getIsFlippedIso()) { + if (token.isFlippedIso()) { if (flipIsoImageMap.get(token) == null) { workImage = IsometricGrid.isoImage(workImage); } else { @@ -2330,7 +2330,7 @@ protected void renderTokens( timer.stop("tokenlist-5"); timer.start("tokenlist-5a"); - if (token.getIsFlippedIso()) { + if (token.isFlippedIso()) { if (flipIsoImageMap.get(token) == null) { workImage = IsometricGrid.isoImage(workImage); flipIsoImageMap.put(token, workImage); diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index a27c3787b2..6a85d27c6e 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -205,7 +205,7 @@ public enum Update { setVisible, setVisibleOnlyToOwner, setIsAlwaysVisible, - setTokenOpacity, + setOpacity, setTerrainModifier, setTerrainModifierOperation, setTerrainModifiersIgnored, @@ -323,9 +323,9 @@ public String toString() { private Set terrainModifiersIgnored = new HashSet<>(Collections.singletonList(TerrainModifierOperation.NONE)); - private boolean isFlippedX; - private boolean isFlippedY; - private Boolean isFlippedIso = false; + private boolean flippedX; + private boolean flippedY; + private Boolean flippedIso = false; private MD5Key charsheetImage; private MD5Key portraitImage; @@ -447,9 +447,9 @@ public Token(Token token) { anchorX = token.anchorX; anchorY = token.anchorY; facing = token.facing; - isFlippedX = token.isFlippedX; - isFlippedY = token.isFlippedY; - isFlippedIso = token.isFlippedIso; + flippedX = token.flippedX; + flippedY = token.flippedY; + flippedIso = token.flippedIso; imageRotation = token.imageRotation; scaleX = token.scaleX; scaleY = token.scaleY; @@ -587,7 +587,7 @@ public void setImageTableName(String imageTableName) { } public void setWidth(int width) { - if (getIsFlippedIso()) { + if (isFlippedIso()) { isoWidth = width; } else { this.width = width; @@ -595,7 +595,7 @@ public void setWidth(int width) { } public void setHeight(int height) { - if (getIsFlippedIso()) { + if (isFlippedIso()) { isoHeight = height; } else { this.height = height; @@ -603,7 +603,7 @@ public void setHeight(int height) { } public int getWidth() { - if (getIsFlippedIso() && isoWidth != 0) { + if (isFlippedIso() && isoWidth != 0) { return isoWidth; } else { return width; @@ -611,7 +611,7 @@ public int getWidth() { } public int getHeight() { - if (getIsFlippedIso() && isoHeight != 0) { + if (isFlippedIso() && isoHeight != 0) { return isoHeight; } else { return height; @@ -1461,17 +1461,17 @@ public Area getTransformedMaskTopology(Area areaToTransform) { atArea.concatenate(AffineTransform.getScaleInstance(scalerX, scalerY)); // Lets account for flipped images... - if (isFlippedX) { + if (flippedX) { atArea.concatenate(AffineTransform.getScaleInstance(-1.0, 1.0)); atArea.concatenate(AffineTransform.getTranslateInstance(-getWidth(), 0)); } - if (isFlippedY) { + if (flippedY) { atArea.concatenate(AffineTransform.getScaleInstance(1.0, -1.0)); atArea.concatenate(AffineTransform.getTranslateInstance(0, -getHeight())); } - if (getIsFlippedIso()) { + if (isFlippedIso()) { return new Area(atArea.createTransformedShape(IsometricGrid.isoArea(areaToTransform))); } @@ -2143,31 +2143,31 @@ public void setAnchorY(int yAnchor) { this.anchorY = yAnchor; } - public boolean getIsFlippedIso() { - if (isFlippedIso != null) { - return isFlippedIso; + public boolean isFlippedIso() { + if (flippedIso != null) { + return flippedIso; } return false; } - public void setIsFlippedIso(boolean isFlippedIso) { - this.isFlippedIso = isFlippedIso; + public void setFlippedIso(boolean flippedIso) { + this.flippedIso = flippedIso; } public boolean isFlippedX() { - return isFlippedX; + return flippedX; } public void setFlippedX(boolean flippedX) { - this.isFlippedX = flippedX; + this.flippedX = flippedX; } public boolean isFlippedY() { - return isFlippedY; + return flippedY; } public void setFlippedY(boolean flippedY) { - this.isFlippedY = flippedY; + this.flippedY = flippedY; } public double getImageRotation() { @@ -2593,8 +2593,8 @@ protected Object readResolve() { if (speechMap == null) { speechMap = new HashMap<>(); } - if (isFlippedIso == null) { - isFlippedIso = false; + if (flippedIso == null) { + flippedIso = false; } if (hasImageTable == null) { hasImageTable = false; @@ -2844,7 +2844,7 @@ public void updateProperty(Zone zone, Update update, List case setIsAlwaysVisible: setIsAlwaysVisible(parameters.get(0).getBoolValue()); break; - case setTokenOpacity: + case setOpacity: setOpacity(Float.parseFloat(parameters.get(0).getStringValue())); break; case setTerrainModifier: @@ -2942,7 +2942,7 @@ public void updateProperty(Zone zone, Update update, List setFlippedY(!isFlippedY()); break; case flipIso: - setIsFlippedIso(!getIsFlippedIso()); + setFlippedIso(!isFlippedIso()); break; } if (lightChanged) { @@ -2989,9 +2989,9 @@ public static Token fromDto(TokenDto dto) { token.scaleX = dto.getScaleX(); token.scaleY = dto.getScaleY(); token.sizeScale = dto.getSizeScale(); - token.isFlippedX = dto.getFlippedX(); - token.isFlippedY = dto.getFlippedY(); - token.isFlippedIso = dto.getFlippedIso(); + token.flippedX = dto.getFlippedX(); + token.flippedY = dto.getFlippedY(); + token.flippedIso = dto.getFlippedIso(); token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null; dto.getSizeMapMap().forEach((k, v) -> token.sizeMap.put(k, GUID.valueOf(v))); @@ -3153,9 +3153,9 @@ public TokenDto toDto() { terrainModifiersIgnored.stream() .map(m -> TerrainModifierOperationDto.valueOf(m.name())) .collect(Collectors.toList())); - dto.setFlippedX(isFlippedX); - dto.setFlippedY(isFlippedY); - dto.setFlippedIso(isFlippedIso); + dto.setFlippedX(flippedX); + dto.setFlippedY(flippedY); + dto.setFlippedIso(flippedIso); if (charsheetImage != null) { dto.setCharsheetImage(StringValue.of(charsheetImage.toString())); } diff --git a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java index a2fa0ee24c..5278d3161d 100644 --- a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java +++ b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java @@ -27,10 +27,7 @@ import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; -import java.awt.geom.Area; -import java.awt.geom.GeneralPath; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; +import java.awt.geom.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,6 +41,7 @@ import net.rptools.maptool.client.ui.theme.Images; import net.rptools.maptool.client.ui.theme.RessourceManager; import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.model.GridFactory; /** */ public class GraphicsUtil { @@ -464,4 +462,58 @@ protected void paintComponent(Graphics g) { f.setVisible(true); // System.out.println(area.equals(area2)); } + + public static Shape createGridShape(String gridType, double size) { + Shape gridShape; + int sides = 0; + double startAngle = 0; + double increment; + double skew = 0; + double hScale = 1; + double vScale = 1; + double root3 = Math.sqrt(3d); + switch (gridType) { + case GridFactory.HEX_HORI -> { + sides = 6; + startAngle = Math.TAU / 12; + hScale = vScale = root3 / 4d; + } + case GridFactory.HEX_VERT -> { + sides = 6; + hScale = vScale = Math.sqrt(3d / 2d); + } + case GridFactory.ISOMETRIC -> { + sides = 4; + vScale = 0.5; + } + case GridFactory.ISOMETRIC_HEX -> { + sides = 6; + startAngle = Math.TAU / 24; + hScale = vScale = Math.sqrt(3d / 2d); + skew = Math.toRadians(30d); + } + case GridFactory.NONE -> { + return new Ellipse2D.Double(-size / 2d, -size / 2d, size, size); + } + case GridFactory.SQUARE -> { + sides = 4; + startAngle = Math.TAU / 8d; + } + } + increment = Math.TAU / sides; + Path2D path = new Path2D.Double(); + path.moveTo(Math.cos(startAngle) * size * hScale, Math.sin(startAngle) * size * vScale); + for (int i = 1; i < sides; i++) { + path.lineTo( + Math.cos(startAngle + i * increment) * size * hScale, + Math.sin(startAngle + i * increment) * size * vScale); + } + path.closePath(); + if (skew != 0) { + gridShape = AffineTransform.getShearInstance(skew, 0).createTransformedShape(path); + } else { + gridShape = path; + } + return path; + } } diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto index 11fa726195..2a092bcc95 100644 --- a/src/main/proto/data_transfer_objects.proto +++ b/src/main/proto/data_transfer_objects.proto @@ -640,7 +640,7 @@ enum TokenUpdateDto { setVisible = 34; setVisibleOnlyToOwner = 35; setIsAlwaysVisible = 36; - setTokenOpacity = 37; + setOpacity = 37; setTerrainModifier = 38; setTerrainModifierOperation = 39; setTerrainModifiersIgnored = 40; diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 5826278b48..9b3924c538 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -128,6 +128,7 @@ Button.install = Install Button.runmacro = RUN MACRO Button.hide = Hide +Label.footprint = Footprint Label.lights = Vision: Label.name = Name: Label.gmname = GM Name: @@ -202,10 +203,29 @@ Label.board = Board Label.visibility = Visibility Label.pathfilename = Path/Filename: Label.allowURIAccess = Allow URI Access +Label.rotation = Rotation +Label.snapToGrid = Snap to Grid +Label.zoom = Zoom + +Mouse.leftClick = Left-click +Mouse.leftDoubleClick = Double-left-click +Mouse.rightClick = Right-click +Mouse.rightDoubleClick = Double-right-click +Mouse.shiftLeftClick = Shift-left-click +Mouse.shiftRightClick = Shift-right-click +Mouse.ctrlLeftClick = Ctrl-left-click +Mouse.ctrlRightClick = Ctrl-right-click +Mouse.altLeftClick = Alt-left-click +Mouse.altRightClick = Alt-right-click +Mouse.leftDrag = Left-drag +Mouse.rightDrag = Right-drag +Mouse.wheel = Mouse-wheel +Mouse.shiftWheel = Shift-mouse-wheel +Mouse.ctrlWheel = Ctrl-mouse-wheel +Mouse.altWheel = Alt-mouse-wheel CampaignPropertyDialog.error.parenthesis = Missing right parenthesis ")" in the following property: {0} -ColorPicker.tooltip.gridSnap = Snap to Grid ColorPicker.tooltip.lineType = Line Type ColorPicker.tooltip.eraser = Eraser ColorPicker.tooltip.penWidth = Pen Width @@ -273,7 +293,7 @@ EditTokenDialog.label.gmnotes = GM Notes EditTokenDialog.menu.notes.sendChat = Send to Chat EditTokenDialog.menu.notes.sendEmit = Send as Emit EditTokenDialog.label.shape = Shape: -EditTokenDialog.label.snaptogrid = Snap To Grid: + EditTokenDialog.label.size = Size: EditTokenDialog.label.visible = Visible to players: EditTokenDialog.label.properties = Properties: @@ -285,7 +305,7 @@ EditTokenDialog.label.image = Image Table: EditTokenDialog.label.uniqueLightSources = Unique Light Sources: EditTokenDialog.label.opacity = Token Opacity: EditTokenDialog.label.opacity.tooltip = Change the opacity of the token. -EditTokenDialog.label.opacity.100 = 100% +EditTokenDialog.label.opacity.100 = 100% EditTokenDialog.label.terrain.ignore = Ignore Terrain: EditTokenDialog.label.terrain.ignore.tooltip= Select any terrain modifier types this token should ignore. EditTokenDialog.label.statSheet = Stat Sheet @@ -295,7 +315,7 @@ EditTokenDialog.border.title.layout = Layout EditTokenDialog.border.title.portrait = Portrait EditTokenDialog.border.title.handout = Handout EditTokenDialog.border.title.charsheet = Charsheet -EditTokenDialog.status.layout.instructions = Mouse Wheel to zoom; double-LClick to reset position and zoom +EditTokenDialog.status.layout.instructions = Mouse Wheel to scale; double-LClick to reset position and zoom EditTokenDialog.label.allplayers = All Players EditTokenDialog.tab.properties = Properties EditTokenDialog.tab.vbl = Topology @@ -371,6 +391,17 @@ EditTokenDialog.button.hero.refresh.tooltip.off = Refresh data from Hero L EditTokenDialog.libTokenURI.error.notLibToken = {0} is not a valid name for a lib:Token. EditTokenDialog.libTokenURI.error.reserved = lib:Tokens can not be named {0} if you want to enable URI access. +EditTokenDialog.layout.help.caption = Mouse Controls +EditTokenDialog.layout.help.moveView = Move the viewport. +EditTokenDialog.layout.help.zoomView = Magnify the viewport. +EditTokenDialog.layout.help.rotateImage = Adjust the rotation of the token image. +EditTokenDialog.layout.help.scaleImage = Change the size of the token image. +EditTokenDialog.layout.help.moveImage = Change the position of the token image. +EditTokenDialog.layout.help.reset = Revert to starting values. +EditTokenDialog.layout.help.resetDefaults = Revert to default values. +EditTokenDialog.layout.scaleButton.tooltip = Switch between horizontal, vertical, or both. + + MapPropertiesDialog.label.playerMapAlias = Display Name: MapPropertiesDialog.label.playerMapAlias.tooltip = This is how players will see the map identified. Must be unique within campaign. MapPropertiesDialog.label.Name.tooltip = This is how the map is referred to in macros and is only visible to the GM. @@ -814,7 +845,6 @@ CampaignPropertiesDialog.button.importPredefined = Import Predefined CampaignPropertiesDialog.button.importPredefined.tooltip = Import predefined properties and settings for the selected system CampaignPropertiesDialog.button.import.tooltip = Import predefined properties from a file CampaignPropertiesDialog.button.export.tooltip = Export campaign properties to file -CampaignPropertiesDialog.export.message = Campaign properties will be exported in JSON format and cannot be imported. CampaignPropertiesDialog.combo.importPredefined.tooltip = System to import properties for CampaignPropertiesDialog.button.import = Import Predefined campaignPropertiesDialog.newTokenTypeName = Enter the name for the new token type @@ -2017,8 +2047,6 @@ macro.function.general.unknownProperty = Error executing "{0}": the macro.function.general.unknownTokenOnMap = Error executing "{0}": the token name or id "{1}" is unknown on map "{2}". macro.function.general.wrongNumParam = Function "{0}" requires exactly {1} parameters; {2} were provided. macro.function.general.listCannotBeEmpty = {0}: string list at argument {1} cannot be empty -macro.function.general.wrongParamType = A parameter is the wrong type. -macro.function.general.paramCannotBeEmpty = A parameter is empty. # Token Distance functions # I.e. ONE_TWO_ONE or ONE_ONE_ONE macro.function.getDistance.invalidMetric = Invalid metric type "{0}". @@ -2663,7 +2691,6 @@ token.popup.menu.lights.clearAll = Clear All token.popup.menu.auras.clear = Clear Auras Only token.popup.menu.auras.clearGM = Clear GM Auras Only token.popup.menu.auras.clearOwner = Clear Owner Auras Only -token.popup.menu.snap = Snap to grid token.popup.menu.visible = Visible to players token.popup.menu.impersonate = Impersonate token.popup.menu.move = Move From 140697367cee623b7a9deb4a4c3252e47b239aa3 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:13:46 +0800 Subject: [PATCH 05/24] Forgot I hadn't finished hexes --- src/main/java/net/rptools/maptool/util/GraphicsUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java index 5278d3161d..0e723a8c4c 100644 --- a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java +++ b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java @@ -476,11 +476,11 @@ public static Shape createGridShape(String gridType, double size) { case GridFactory.HEX_HORI -> { sides = 6; startAngle = Math.TAU / 12; - hScale = vScale = root3 / 4d; + hScale = vScale = root3 / 3d; } case GridFactory.HEX_VERT -> { sides = 6; - hScale = vScale = Math.sqrt(3d / 2d); + hScale = vScale = root3 / 3d; } case GridFactory.ISOMETRIC -> { sides = 4; @@ -489,7 +489,7 @@ public static Shape createGridShape(String gridType, double size) { case GridFactory.ISOMETRIC_HEX -> { sides = 6; startAngle = Math.TAU / 24; - hScale = vScale = Math.sqrt(3d / 2d); + hScale = vScale = root3 / 3d; skew = Math.toRadians(30d); } case GridFactory.NONE -> { From aedb0607c1bb24e0e6b21ee2c22da8cd4c2b9d8c Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:38:14 +0800 Subject: [PATCH 06/24] Changed all comments to block comments to make it easier to find commented code. Commented code removed --- .../ui/token/dialog/edit/EditTokenDialog.java | 219 +++++++----------- 1 file changed, 87 insertions(+), 132 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java index 15f06a3ba6..ad7b0f8d88 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java @@ -109,7 +109,7 @@ public class EditTokenDialog extends AbeillePanel { RessourceManager.getBigIcon(Icons.EDIT_TOKEN_REFRESH_ON); private static final ImageIcon REFRESH_ICON_OFF = RessourceManager.getBigIcon(Icons.EDIT_TOKEN_REFRESH_OFF); - // private CharSheetController controller; + private final RSyntaxTextArea xmlStatblockRSyntaxTextArea = new RSyntaxTextArea(2, 2); private final RSyntaxTextArea textStatblockRSyntaxTextArea = new RSyntaxTextArea(2, 2); private final WordWrapCellRenderer propertyCellRenderer = new WordWrapCellRenderer(); @@ -119,7 +119,6 @@ public class EditTokenDialog extends AbeillePanel { private ImageAssetPanel imagePanel; private final LibraryManager libraryManager = new LibraryManager(); - // private final Toolbox toolbox = new Toolbox(); private HeroLabData heroLabData; private AutoGenerateTopologySwingWorker autoGenerateTopologySwingWorker = new AutoGenerateTopologySwingWorker(false, Color.BLACK); @@ -234,7 +233,7 @@ public void closeDialog() { validateLibTokenURIAccess(getNameField().getName()); var combo = getStatSheetCombo(); combo.removeAllItems(); - // Default Entry + /* Default Entry */ var defaultSS = new StatSheet(null, I18N.getText("token.statSheet.useDefault"), null, Set.of(), null); combo.addItem(defaultSS); @@ -280,27 +279,26 @@ private void validateLibTokenURIAccess(String name) { @Override public void bind(final Token token) { - // ICON + /* ICON */ getTokenIconPanel().setImageId(token.getImageAssetId()); - - // NOTES, GM NOTES. Due to the way things happen on different gui threads, the type must be set - // before the text - // otherwise the wrong values can get populated when the tab change listener fires. + /* NOTES, GM NOTES. Due to the way things happen on different gui threads, the type must be set + before the text + otherwise the wrong values can get populated when the tab change listener fires. */ getGMNotesEditor().setTextType(token.getGmNotesType()); getGMNotesEditor().setText(token.getGMNotes()); getPlayerNotesEditor().setTextType(token.getNotesType()); getPlayerNotesEditor().setText(token.getNotes()); - // TYPE + /* TYPE */ getTypeCombo().setSelectedItem(token.getType()); - // SIGHT + /* SIGHT */ updateSightTypeCombo(); - // Image Tables + /* Image Tables */ updateImageTableCombo(); - // STATES + /* STATES */ Component barPanel = null; updateStatesPanel(); Component[] statePanels = getStatesPanel().getComponents(); @@ -316,7 +314,7 @@ public void bind(final Token token) { } } - // BARS + /* BARS */ if (barPanel != null) { Component[] barComponents = ((Container) barPanel).getComponents(); JCheckBox cb = null; @@ -344,32 +342,27 @@ public void bind(final Token token) { } } - // OWNER LIST + /* OWNER LIST */ EventQueue.invokeLater(() -> getOwnerList().setModel(new OwnerListModel())); - // SPEECH TABLE + /* SPEECH TABLE */ EventQueue.invokeLater(() -> getSpeechTable().setModel(new SpeechTableModel(token))); - // Player player = MapTool.getPlayer(); - // boolean editable = player.isGM() || - // !MapTool.getServerPolicy().useStrictTokenManagement() || - // token.isOwner(player.getName()); - // getAllPlayersCheckBox().setSelected(token.isOwnedByAll()); - - // OTHER + /* OTHER */ getShapeCombo().setSelectedItem(token.getShape()); setSizeCombo(token); getSnapToGrid().setSelected(token.isSnapToGrid()); getFlippedIso().setSelected(token.isFlippedIso()); getFlippedX().setSelected(token.isFlippedX()); getFlippedY().setSelected(token.isFlippedY()); - // Updates the Property Type list. + + /* Updates the Property Type list. */ updatePropertyTypeCombo(); - // Set the selected item in Property Type list. Triggers a itemStateChanged event if index != 0 + /* Set the selected item in Property Type list. Triggers a itemStateChanged event if index != 0 */ getPropertyTypeCombo().setSelectedItem(token.getPropertyType()); - // If index == 0, the itemStateChanged event wasn't triggered, so we update. Fix #1504 + /* If index == 0, the itemStateChanged event wasn't triggered, so we update. Fix #1504 */ if (getPropertyTypeCombo().getSelectedIndex() == 0) { updatePropertiesTable((String) getPropertyTypeCombo().getSelectedItem()); } @@ -388,7 +381,7 @@ public void bind(final Token token) { getTerrainModifier().setText(Double.toString(token.getTerrainModifier())); getTerrainModifierOperationComboBox().setSelectedItem(token.getTerrainModifierOperation()); - // Get tokens ignored list, match to the index in the JList then select them. + /* Get tokens ignored list, match to the index in the JList then select them. */ getTerrainModifiersIgnoredList() .setSelectedIndices( token.getTerrainModifiersIgnored().stream() @@ -404,7 +397,7 @@ public void bind(final Token token) { getUniqueLightSourcesTextPane() .setText(new LightSyntax().stringifyLights(token.getUniqueLightSources())); - // Jamz: Init the Topology tab... + /* Jamz: Init the Topology tab... */ JTabbedPane tabbedPane = getTabbedPane(); String topologyTitle = I18N.getText("EditTokenDialog.tab.vbl"); @@ -418,7 +411,7 @@ public void bind(final Token token) { getVisibilityToleranceSpinner().setValue(token.getAlwaysVisibleTolerance()); getJtsMethodComboBox().setSelectedItem(getTokenTopologyPanel().getJtsMethod()); - // Reset scale + /* Reset scale */ getTokenTopologyPanel().setScale(1d); } else { tabbedPane.setEnabledAt(tabbedPane.indexOfTab(topologyTitle), false); @@ -461,7 +454,7 @@ public void bind(final Token token) { getAllowURLAccess().setSelected(token.getAllowURIAccess()); - // Jamz: Init the Hero Lab tab... + /* Jamz: Init the Hero Lab tab... */ heroLabData = token.getHeroLabData(); String heroLabTitle = I18N.getString("EditTokenDialog.tab.hero"); @@ -508,8 +501,6 @@ public void bind(final Token token) { ((JLabel) getComponent("lastModified")).setText(heroLabData.getLastModifiedDateString()); EventQueue.invokeLater(this::loadHeroLabImageList); - - // loadHeroLabImageList(); } else { tabbedPane.setEnabledAt(tabbedPane.indexOfTab(heroLabTitle), false); if (tabbedPane.getSelectedIndex() == tabbedPane.indexOfTab(heroLabTitle)) { @@ -517,8 +508,8 @@ public void bind(final Token token) { } } - // we will disable the Owner only visible check box if the token is not - // visible to players to signify the relationship + /* we will disable the Owner only visible check box if the token is not + visible to players to signify the relationship */ ActionListener tokenVisibleActionListener = actionEvent -> { AbstractButton abstractButton = (AbstractButton) actionEvent.getSource(); @@ -528,23 +519,6 @@ public void bind(final Token token) { }; getVisibleCheckBox().addActionListener(tokenVisibleActionListener); - // Character Sheets - // controller = null; - // String form = - // MapTool.getCampaign().getCharacterSheets().get(token.getPropertyType()); - // if (form == null) - // return; - // URL formUrl = getClass().getClassLoader().getResource(form); - // if (formUrl == null) - // return; - // controller = new CharSheetController(formUrl, null); - // HashMap properties = new HashMap(); - // for (String prop : token.getPropertyNames()) - // properties.put(prop, token.getProperty(prop)); - // controller.setData(properties); - // controller.getPanel().setName("characterSheet"); - // replaceComponent("sheetPanel", "characterSheet", controller.getPanel()); - super.bind(token); } @@ -622,6 +596,7 @@ public void initTokenIconPanel() { public ImageAssetPanel getTokenIconPanel() { if (imagePanel == null) { imagePanel = new ImageAssetPanel(); + imagePanel.setAllowEmptyImage(false); replaceComponent("mainPanel", "tokenImage", imagePanel); } @@ -784,30 +759,30 @@ public boolean commit() { if (getPropertyTable().isEditing()) { getPropertyTable().getCellEditor().stopCellEditing(); } - // Commit the changes to the token properties - // If no map available, cancel the commit. Fixes #1646. + /* Commit the changes to the token properties */ + /* If no map available, cancel the commit. Fixes #1646. */ if (!super.commit() || MapTool.getFrame().getCurrentZoneRenderer() == null) { return false; } - // TYPE - // Only update this if it actually changed + /* TYPE */ + /* Only update this if it actually changed */ if (getTypeCombo().getSelectedItem() != token.getType()) { token.setType((Token.Type) getTypeCombo().getSelectedItem()); } - // NOTES + /* NOTES */ token.setGMNotes(getGMNotesEditor().getText()); token.setGmNotesType(getGMNotesEditor().getTextType()); token.setNotes(getPlayerNotesEditor().getText()); token.setNotesType(getPlayerNotesEditor().getTextType()); - // SIZE + /* SIZE */ token.setSnapToScale(getSizeCombo().getSelectedIndex() != 0); if (getSizeCombo().getSelectedIndex() > 0) { Grid grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); token.setFootprint(grid, (TokenFootprint) getSizeCombo().getSelectedItem()); } - // Other + /* Other */ token.setPropertyType((String) getPropertyTypeCombo().getSelectedItem()); token.setSightType((String) getSightTypeCombo().getSelectedItem()); token.setImageTableName((String) getImageTableCombo().getSelectedItem()); @@ -819,7 +794,7 @@ public boolean commit() { try { token.setTerrainModifier(Double.parseDouble(getTerrainModifier().getText())); } catch (NumberFormatException e) { - // User didn't enter a valid float... + /* User didn't enter a valid float... */ token.setTerrainModifier(1); } @@ -837,7 +812,7 @@ public boolean commit() { token.addUniqueLightSource(lightSource); } - // Get the states + /* Get the states */ Component[] stateComponents = getStatesPanel().getComponents(); Container barPanel = null; for (Component stateComponent : stateComponents) { @@ -851,9 +826,9 @@ public boolean commit() { String state = cb.getText(); token.setState(state, cb.isSelected() ? Boolean.TRUE : Boolean.FALSE); } - } // endfor + } - // BARS + /* BARS */ if (barPanel != null) { for (var barContainer : barPanel.getComponents()) { var barComponents = ((Container) barContainer).getComponents(); @@ -870,9 +845,8 @@ public boolean commit() { * 100)); } } - // Ownership - // If the token is owned by all and we are a player don't alter the ownership - // list. + /* OWNERSHIP */ + /* If the token is owned by all and we are a player don't alter the ownership list. */ if (MapTool.getPlayer().isGM() || !token.isOwnedByAll()) { token.clearAllOwners(); @@ -883,8 +857,7 @@ public boolean commit() { token.addOwner((String) selectable.getObject()); } } - // If we are not a GM and the only non GM owner make sure we can't - // take our selves off of the owners list + /* If we are not a GM and we are the only non-GM owner, make sure we cannot remove ourself from the owners list */ if (!MapTool.getPlayer().isGM()) { boolean hasPlayer = token.isOwnedByAny(MapTool.getNonGMs()); if (!hasPlayer) { @@ -892,10 +865,10 @@ public boolean commit() { } } } - // SHAPE + /* SHAPE */ token.setShape((Token.TokenShape) getShapeCombo().getSelectedItem()); - // Stat Sheet + /* Stat Sheet */ var ss = (StatSheet) getStatSheetCombo().getSelectedItem(); if (ss == null || (ss.name() == null && ss.namespace() == null)) { token.useDefaultStatSheet(); @@ -908,38 +881,38 @@ public boolean commit() { token.setStatSheet(new StatSheetProperties(ssManager.getId(ss), location)); } - // Macros + /* Macros */ token.setSpeechMap(((KeyValueTableModel) getSpeechTable().getModel()).getMap()); - // Properties + /* Properties */ ((TokenPropertyTableModel) getPropertyTable().getModel()).applyTo(token); - // Charsheet + /* Charsheet */ if (getCharSheetPanel().getImageId() != null) { MapToolUtil.uploadAsset(AssetManager.getAsset(getCharSheetPanel().getImageId())); } token.setCharsheetImage(getCharSheetPanel().getImageId()); - // IMAGE + /* IMAGE */ if (!token.getImageAssetId().equals(getTokenIconPanel().getImageId())) { MapToolUtil.uploadAsset(AssetManager.getAsset(getTokenIconPanel().getImageId())); token.setImageAsset(null, getTokenIconPanel().getImageId()); // Default image for now } - // PORTRAIT + /* PORTRAIT */ if (getPortraitPanel().getImageId() != null) { - // Make sure the server has the image + /* Make sure the server has the image */ if (!MapTool.getCampaign().containsAsset(getPortraitPanel().getImageId())) { MapTool.serverCommand().putAsset(AssetManager.getAsset(getPortraitPanel().getImageId())); } } token.setPortraitImage(getPortraitPanel().getImageId()); - // LAYOUT + /* LAYOUT */ getTokenLayoutPanel().getHelper().commitChanges(token); token.setSnapToGrid(getSnapToGrid().isSelected()); - // TOPOLOGY + /* TOPOLOGY */ for (final var type : Zone.TopologyType.values()) { token.setMaskTopology(type, getTokenTopologyPanel().getTopology(type)); } @@ -953,25 +926,20 @@ public boolean commit() { token.setHeroLabData(heroLabData); - // URI Access + /* URI Access */ token.setAllowURIAccess(getAllowURLAccess().isEnabled() && getAllowURLAccess().isSelected()); - // OTHER + /* OTHER */ tokenSaved = true; - // Character Sheet - // Map properties = controller.getData(); - // for (String prop : token.getPropertyNames()) - // token.setProperty(prop, properties.get(prop)); - - // Update UI + /* Update UI */ MapTool.getFrame().updateTokenTree(); MapTool.getFrame().resetTokenPanels(); // Jamz: TODO check if topology changed on token first - // MapTool.getFrame() - // .getCurrentZoneRenderer() - // .getZone() - // .tokenMaskTopologyChanged(token.getMaskTopologyTypes()); + MapTool.getFrame() + .getCurrentZoneRenderer() + .getZone() + .tokenMaskTopologyChanged(token.getMaskTopologyTypes()); return true; } @@ -997,7 +965,7 @@ public PropertyTable getPropertyTable() { } private void updateStatesPanel() { - // Group the states first into individual panels + /* Group the states first into individual panels */ List overlays = new ArrayList(MapTool.getCampaign().getTokenStatesMap().values()); Map groups = new TreeMap(); @@ -1019,13 +987,13 @@ private void updateStatesPanel() { } } - // Add the group panels and bar panel to the states panel + /* Add the group panels and bar panel to the states panel */ JPanel statesPanel = getStatesPanel(); MigLayout layout = new MigLayout("wrap", "[fill,grow]"); statesPanel.setLayout(layout); statesPanel.removeAll(); - // Add the individual check boxes. + /* Add the individual check boxes. */ for (BooleanTokenOverlay state : overlays) { String group = state.getGroup(); var panel = groups.get(""); @@ -1043,7 +1011,7 @@ private void updateStatesPanel() { JPanel barPanel = new JPanel(new MigLayout("wrap 2", "[fill,grow][fill,grow]")); barPanel.setName("bar"); - // Add sliders to the bar panel + /* Add sliders to the bar panel */ if (!MapTool.getCampaign().getTokenBarsMap().isEmpty()) { barPanel.setBorder( BorderFactory.createTitledBorder(I18N.getText("CampaignPropertiesDialog.tab.bars"))); @@ -1258,10 +1226,10 @@ public String getToolTipText(MouseEvent event) { } }; propertyTable.setFillsViewportHeight(true); // XXX This is Java6-only -- need - // Java5 solution + /* Java5 solution */ propertyTable.setName("propertiesTable"); - // wrap button and functionality + /* wrap button and functionality */ JPanel buttonsAndPropertyTable = new JPanel(); buttonsAndPropertyTable.setLayout(new BorderLayout()); JCheckBox wrapToggle = new JCheckBox(I18N.getString("EditTokenDialog.msg.wrap")); @@ -1273,19 +1241,13 @@ public String getToolTipText(MouseEvent event) { buttonsAndPropertyTable.add(wrapToggle, BorderLayout.PAGE_END); PropertyPane pane = new PropertyPane(propertyTable); - // pane.setPreferredSize(new Dimension(100, 300)); buttonsAndPropertyTable.add(pane, BorderLayout.CENTER); replaceComponent("propertiesPanel", "propertiesTable", buttonsAndPropertyTable); } - public void initTokenDetails() { - // tokenGMNameLabel = panel.getLabel("tokenGMNameLabel"); - } - public void initTokenLayoutPanel() { TokenLayoutRenderPanel layoutPanel = new TokenLayoutRenderPanel(); - TokenLayoutPanelHelper layoutHelper = - new TokenLayoutPanelHelper(this, layoutPanel, getOKButton()); + new TokenLayoutPanelHelper(this, layoutPanel, getOKButton()); layoutPanel.setMinimumSize(new Dimension(150, 125)); layoutPanel.setPreferredSize(new Dimension(150, 125)); layoutPanel.setName("tokenLayout"); @@ -1615,25 +1577,25 @@ public void loadHeroLabImageList() { * Initialize the Hero Lab statblock tabs */ public void initStatBlocks() { - // Setup the HTML panel + /* Setup the HTML panel */ JEditorPane statblockPane = getHtmlStatblockEditor(); HTMLEditorKit kit = new HTMLEditorKit(); HTMLDocument statblockDoc = (HTMLDocument) kit.createDefaultDocument(); statblockPane.setEditorKit(kit); statblockPane.setDocument(statblockDoc); - // We need this property as the kit can't handle { @@ -1701,7 +1663,7 @@ public void initStatBlocks() { textStatblockRSyntaxTextArea.setText(heroLabData.getStatBlock_text()); textStatblockRSyntaxTextArea.setCaretPosition(0); - // Update the images + /* Update the images */ MD5Key tokenImageKey = heroLabData.getTokenImage(); if (tokenImageKey != null) { getTokenIconPanel().setImageId(tokenImageKey); @@ -1718,8 +1680,8 @@ public void initStatBlocks() { getCharSheetPanel().setImageId(handoutAssetKey); } - // If NPC, lets not overwrite the Name, it may be "Creature 229" or such, GM - // name is enough + /* If NPC, lets not overwrite the Name, it may be "Creature 229" or such, GM + name is enough */ ((JTextField) getComponent("@GMName")).setText(heroLabData.getName()); if (heroLabData.isAlly()) { getTypeCombo().setSelectedItem(Type.PC); @@ -1728,13 +1690,13 @@ public void initStatBlocks() { getTypeCombo().setSelectedItem(Type.NPC); } - // Update image list + /* Update image list */ loadHeroLabImageList(); } } }); - // Setup xPath searching for XML StatBlock + /* Setup xPath searching for XML StatBlock */ JTextField xmlStatblockSearchTextField = (JTextField) getComponent("xmlStatblockSearchTextField"); JButton xmlStatblockSearchButton = (JButton) getComponent("xmlStatblockSearchButton"); @@ -1768,7 +1730,7 @@ public void keyPressed(KeyEvent e) { xmlStatblockRSyntaxTextArea.setCaretPosition(0); }); - // Setup regular expression searching for TEXT StatBlock + /* Setup regular expression searching for TEXT StatBlock */ JTextField textStatblockSearchTextField = (JTextField) getComponent("textStatblockSearchTextField"); JButton textStatblockSearchButton = (JButton) getComponent("textStatblockSearchButton"); @@ -1795,9 +1757,6 @@ public void keyPressed(KeyEvent e) { SearchContext context = new SearchContext(); context.setSearchFor(searchText); context.setRegularExpression(true); - // context.setMatchCase(matchCaseCB.isSelected()); - // context.setSearchForward(forward); - // context.setWholeWord(false); SearchEngine.find(textStatblockRSyntaxTextArea, context).wasFound(); }); @@ -1924,7 +1883,7 @@ public Map getMap() { } } - // needed to change the popup for properties + /* needed to change the popup for properties */ private static class MTMultilineStringExComboBox extends MultilineStringExComboBox { final ResourceBundle a = ResourceBundle.getBundle("com.jidesoft.combobox.combobox"); @@ -1941,7 +1900,7 @@ public PopupPanel createPopupComponent() { } } - // the cell editor for property popups + /* the cell editor for property popups */ private static class MTMultilineStringCellEditor extends MultilineStringCellEditor { protected MTMultilineStringExComboBox createMultilineStringComboBox() { @@ -1953,7 +1912,7 @@ protected MTMultilineStringExComboBox createMultilineStringComboBox() { } } - // the property popup table + /* the property popup table */ private static class MTMultilineStringPopupPanel extends PopupPanel { private RSyntaxTextArea j = createTextArea(); @@ -1964,7 +1923,7 @@ public MTMultilineStringPopupPanel() { public MTMultilineStringPopupPanel(String paramString) { this.setResizable(true); - // Set the color style via Theme + /* Set the color style via Theme */ try { File themeFile = new File( @@ -1999,7 +1958,6 @@ public MTMultilineStringPopupPanel(String paramString) { syntaxComboBox.addActionListener( e -> j.setSyntaxEditingStyle(syntaxComboBox.getSelectedItem().toString())); - // content.add(wrapToggle); add(syntaxComboBox, BorderLayout.BEFORE_FIRST_LINE); add(wrapToggle, BorderLayout.AFTER_LAST_LINE); } @@ -2026,14 +1984,14 @@ protected RSyntaxTextArea createTextArea() { } } - // cell renderer for properties table + /* cell renderer for properties table */ private static class WordWrapCellRenderer extends RSyntaxTextArea implements TableCellRenderer { WordWrapCellRenderer() { setLineWrap(false); setWrapStyleWord(true); - // Set the color style via Theme + /* Set the color style via Theme */ try { File themeFile = new File( @@ -2082,7 +2040,7 @@ protected Void doInBackground() { publish(generatedTopology); } - // Nothing to do, so nothing to publish. + /* Nothing to do, so nothing to publish. */ return null; } @@ -2149,8 +2107,7 @@ public Component getListCellRendererComponent( } } - // // - // HANDLER + /* HANDLER */ public static class MouseHandler extends MouseAdapter { HtmlEditorSplit source; @@ -2198,8 +2155,7 @@ public void mouseClicked(MouseEvent e) { } } - // // - // MODELS + /* MODELS */ private class TokenPropertyTableModel extends AbstractPropertyTableModel implements NavigableModel { @@ -2249,8 +2205,7 @@ public void applyTo(Token token) { @Override public boolean isNavigableAt(int rowIndex, int columnIndex) { - // make the property name column non-navigable so that tab takes you - // directly to the next property value cell. + /* make the property name column non-navigable so that tab takes you directly to the next property value cell. */ return (columnIndex != 0); } From 817739e8c6ee2ed364d6190c3ffd9f4ef451f2aa Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:12:11 +0800 Subject: [PATCH 07/24] Corrected squares in createGridShape. --- .../rptools/maptool/util/GraphicsUtil.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java index 0e723a8c4c..a2c7f9aef0 100644 --- a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java +++ b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java @@ -357,14 +357,9 @@ public static void renderSoftClipping(Graphics2D g, Shape shape, int width, doub RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // Faster without antialiasing, and looks just as good - // float alpha = (float)initialAlpha / width / 6; float alpha = .04f; g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); for (int i = 1; i < width; i += 2) { - // if (alpha * i < .2) { - // // Too faded to see anyway, don't waste cycles on it - // continue; - // } g2.setStroke(new BasicStroke(i)); g2.draw(shape); } @@ -375,8 +370,8 @@ public static Area createLine(int width, Point2D... points) { if (points.length < 2) { throw new IllegalArgumentException("Must supply at least two points"); } - List bottomList = new ArrayList(points.length); - List topList = new ArrayList(points.length); + List bottomList = new ArrayList<>(points.length); + List topList = new ArrayList<>(points.length); for (int i = 0; i < points.length; i++) { double angle = @@ -395,17 +390,13 @@ public static Area createLine(int width, Point2D... points) { double bottomAngle = (angle + delta / 2) % 360; double topAngle = bottomAngle + 180; - // System.out.println(angle + " - " + delta + " - " + bottomAngle + " - " + topAngle); - bottomList.add(getPointAtVector(points[i], bottomAngle, width)); topList.add(getPointAtVector(points[i], topAngle, width)); } - // System.out.println(bottomList); - // System.out.println(topList); Collections.reverse(topList); GeneralPath path = new GeneralPath(); - Point2D initialPoint = bottomList.remove(0); + Point2D initialPoint = bottomList.removeFirst(); path.moveTo((float) initialPoint.getX(), (float) initialPoint.getY()); for (Point2D point : bottomList) { @@ -421,10 +412,6 @@ public static Area createLine(int width, Point2D... points) { private 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)); - - // System.out.println(point + " - " + angle + " - " + x + "x" + y + " - " + - // Math.cos(Math.toRadians(angle)) + " - " + Math.sin(Math.toRadians(angle)) + " - " + - // Math.toRadians(angle)); return new Point2D.Double(x, y); } @@ -433,8 +420,6 @@ public static void main(String[] args) { new Point2D[] { new Point(20, 20), new Point(50, 50), new Point(80, 20), new Point(100, 100) }; - // final Point2D[] points = new Point2D[]{new Point(50, 50), new Point(20, 20), new Point(20, - // 100), new Point(50,75)}; final Area line = createLine(10, points); JFrame f = new JFrame(); @@ -464,14 +449,15 @@ protected void paintComponent(Graphics g) { } public static Shape createGridShape(String gridType, double size) { - Shape gridShape; + final Shape gridShape; int sides = 0; double startAngle = 0; double increment; double skew = 0; double hScale = 1; double vScale = 1; - double root3 = Math.sqrt(3d); + final double root2 = Math.sqrt(2d); + final double root3 = Math.sqrt(3d); switch (gridType) { case GridFactory.HEX_HORI -> { sides = 6; @@ -497,6 +483,7 @@ public static Shape createGridShape(String gridType, double size) { } case GridFactory.SQUARE -> { sides = 4; + hScale = vScale = root2 / 2d; startAngle = Math.TAU / 8d; } } @@ -514,6 +501,6 @@ public static Shape createGridShape(String gridType, double size) { } else { gridShape = path; } - return path; + return gridShape; } } From c7a852d8e8aa1dac9349221b99526147aa57bddf Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:13:22 +0800 Subject: [PATCH 08/24] Added image rotation property to layoutProps functions --- .../functions/TokenPropertyFunctions.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java index b78a0a4a5f..741fad6819 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java @@ -936,6 +936,7 @@ public Object childEvaluate( String scale = String.valueOf(token.getSizeScale()); String xOffset = String.valueOf(token.getAnchorX()); String yOffset = String.valueOf(token.getAnchorY()); + String rotation = String.valueOf(token.getImageRotation()); String scaleX = String.valueOf(token.getScaleX()); String scaleY = String.valueOf(token.getScaleY()); String footprintScaleValue = @@ -948,28 +949,33 @@ public Object childEvaluate( jarr.addProperty("xOffset", xOffset); jarr.addProperty("yOffset", yOffset); jarr.addProperty("scaleX", scaleX); + jarr.addProperty("rotation", rotation); jarr.addProperty("scaleY", scaleY); jarr.addProperty("footprintScale", footprintScaleValue); return jarr; } else { - return "scale=" - + scale - + delim - + "xOffset=" - + xOffset - + delim - + "yOffset=" - + yOffset - + delim - + "scaleX=" - + scaleX - + delim - + "scaleY=" - + scaleY - + delim - + "footprintScale=" - + delim - + footprintScaleValue; + StringBuilder sb = new StringBuilder(); + return sb.append("scale=") + .append(scale) + .append(delim) + .append("xOffset=") + .append(xOffset) + .append(delim) + .append("yOffset=") + .append(yOffset) + .append(delim) + .append("rotation=") + .append(rotation) + .append(delim) + .append("scaleX=") + .append(scaleX) + .append(delim) + .append("scaleY=") + .append(scaleY) + .append(delim) + .append("footprintScale=") + .append(delim) + .append(footprintScaleValue); } } /* @@ -1005,6 +1011,7 @@ public Object childEvaluate( case "yoffset" -> token.setAnchor(token.getAnchorX(), jobj.get(s).getAsInt()); case "scalex" -> token.setScaleX(jobj.get(s).getAsDouble()); case "scaley" -> token.setScaleY(jobj.get(s).getAsDouble()); + case "rotation" -> token.setImageRotation(jobj.get(s).getAsDouble()); default -> { continue; } From cb3b1c28671ea2a3b68aee603eada2f3909030c5 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:20:47 +0800 Subject: [PATCH 09/24] Sanity saving string concatenation --- .../functions/TokenPropertyFunctions.java | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java index 741fad6819..90b8d438a3 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java @@ -955,33 +955,20 @@ public Object childEvaluate( return jarr; } else { StringBuilder sb = new StringBuilder(); - return sb.append("scale=") - .append(scale) - .append(delim) - .append("xOffset=") - .append(xOffset) - .append(delim) - .append("yOffset=") - .append(yOffset) - .append(delim) - .append("rotation=") - .append(rotation) - .append(delim) - .append("scaleX=") - .append(scaleX) - .append(delim) - .append("scaleY=") - .append(scaleY) - .append(delim) - .append("footprintScale=") - .append(delim) - .append(footprintScaleValue); + sb.append("scale=" + scale + delim); + sb.append("xOffset=" + xOffset + delim); + sb.append("yOffset=" + yOffset + delim); + sb.append("rotation=" + rotation + delim); + sb.append("scaleX=" + scaleX + delim); + sb.append("scaleY=" + scaleY + delim); + sb.append("footprintScale=" + footprintScaleValue); + return sb.toString(); } } /* * setExtendedTokenLayoutProps(StrProp/JSON Object, token: currentToken(), mapName = current map) */ - if (functionName.equalsIgnoreCase("setExtendedTokenLayoutProps")) { + if (functionName.equalsIgnoreCase("setExtendedTokenLayoutProps")) { FunctionUtil.checkNumberParam(functionName, parameters, 1, 3); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 1, 2); JsonObject json; From fffb52050de11517e749073d1b77b2fc2e3f5355 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:25:00 +0800 Subject: [PATCH 10/24] Spotless indent --- .../maptool/client/functions/TokenPropertyFunctions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java index 90b8d438a3..bf712f3e46 100644 --- a/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/TokenPropertyFunctions.java @@ -968,7 +968,7 @@ public Object childEvaluate( /* * setExtendedTokenLayoutProps(StrProp/JSON Object, token: currentToken(), mapName = current map) */ - if (functionName.equalsIgnoreCase("setExtendedTokenLayoutProps")) { + if (functionName.equalsIgnoreCase("setExtendedTokenLayoutProps")) { FunctionUtil.checkNumberParam(functionName, parameters, 1, 3); Token token = FunctionUtil.getTokenFromParam(resolver, functionName, parameters, 1, 2); JsonObject json; From 1741bb748321945acefeab9aa95452662722be96 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:26:18 +0800 Subject: [PATCH 11/24] Code cleanup --- .../dialog/edit/TokenLayoutPanelHelper.java | 246 +++++++----------- 1 file changed, 91 insertions(+), 155 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java index a25bca4966..64f8a057e1 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java @@ -22,10 +22,8 @@ import java.awt.event.*; import java.awt.geom.*; import java.awt.image.BufferedImage; -import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; -import java.beans.VetoableChangeListener; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -120,29 +118,29 @@ enum FlipState { Grid grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); RenderBits renderBits = new RenderBits(); boolean isoGrid = grid.isIsometric(); - boolean noGrid = GridFactory.getGridType(grid) == GridFactory.NONE; - boolean squareGrid = GridFactory.getGridType(grid) == GridFactory.SQUARE; + boolean noGrid = GridFactory.getGridType(grid).equals(GridFactory.NONE); + boolean squareGrid = GridFactory.getGridType(grid).equals(GridFactory.SQUARE); boolean hexGrid = false; boolean isIsoFigure = false; int gridSize = grid.getSize(); double cellHeight = grid.getCellHeight(); double cellWidth = grid.getCellWidth(); private static final CellPoint ORIGIN = new CellPoint(0, 0); - private Token token, originalToken, mirrorToken; + private Token originalToken, mirrorToken; private BufferedImage tokenImage; TokenFootprint footprint; Rectangle2D footprintBounds; Set occupiedCells; ArrayList cellCentres; - // controls/components + /* controls/components */ AbeillePanel parent; private JRootPane parentRoot; - private TokenLayoutRenderPanel renderPanel; + private final TokenLayoutRenderPanel renderPanel; private JComboBox sizeCombo; - private JLabel rotationLabel, scaleLabel; + private JLabel scaleLabel; private JSpinner anchorXSpinner, anchorYSpinner, rotationSpinner, scaleSpinner, zoomSpinner; private JSlider anchorXSlider, anchorYSlider, rotationSlider, scaleSlider, zoomSlider; - private AbstractButton scaleButton, layoutHelpButton, okButton; + private AbstractButton scaleButton, okButton; public void setOKButton(JButton b) { okButton = b; @@ -152,37 +150,35 @@ public void setParentRoot(JRootPane rp) { parentRoot = rp; } - private String helpText = assembleHelpText(); + private final String helpText = assembleHelpText(); // @formatter:off // spotless:off - // Component Getters + /* Component Getters */ public JComboBox getSizeCombo(){ if (sizeCombo == null) sizeCombo = (JComboBox) parent.getComponent("size"); return sizeCombo; } - // Labels - public JLabel getRotationLabel(){ if (rotationLabel == null) rotationLabel = parent.getLabel("rotationLabel"); return rotationLabel; } + /* Labels */ public JLabel getScaleLabel(){ if (scaleLabel == null) scaleLabel = parent.getLabel("scaleLabel"); return scaleLabel; } - // Spinners + /* Spinners */ public JSpinner getAnchorXSpinner() { if (anchorXSpinner == null) anchorXSpinner = parent.getSpinner("anchorXSpinner"); return anchorXSpinner; } public JSpinner getAnchorYSpinner() { if (anchorYSpinner == null) anchorYSpinner = parent.getSpinner("anchorYSpinner"); return anchorYSpinner; } public JSpinner getRotationSpinner() { if (rotationSpinner == null) rotationSpinner = parent.getSpinner("rotationSpinner"); return rotationSpinner; } public JSpinner getScaleSpinner() { if (scaleSpinner == null) scaleSpinner = parent.getSpinner("scaleSpinner"); return scaleSpinner; } public JSpinner getZoomSpinner() { if (zoomSpinner == null) zoomSpinner = parent.getSpinner("zoomSpinner"); return zoomSpinner; } - // Sliders + /* Sliders */ public JSlider getAnchorXSlider() { if (anchorXSlider == null) anchorXSlider = (JSlider) parent.getComponent("anchorXSlider"); return anchorXSlider; } public JSlider getAnchorYSlider() { if (anchorYSlider == null) anchorYSlider = (JSlider) parent.getComponent("anchorYSlider"); return anchorYSlider; } public JSlider getRotationSlider() { if (rotationSlider == null) rotationSlider = (JSlider) parent.getComponent("rotationSlider"); return rotationSlider; } public JSlider getScaleSlider() { if (scaleSlider == null) scaleSlider = (JSlider) parent.getComponent("scaleSlider"); return scaleSlider; } public JSlider getZoomSlider() { if (zoomSlider == null) zoomSlider = (JSlider) parent.getComponent("zoomSlider"); return zoomSlider; } - // Buttons + /* Buttons */ public AbstractButton getScaleButton() { if (scaleButton == null) scaleButton = parent.getButton("scaleButton"); return scaleButton; } - public AbstractButton getHelpButton() {if (layoutHelpButton == null) layoutHelpButton = parent.getButton("layoutHelpButton"); return layoutHelpButton; } - // Panel + /* Panel */ public TokenLayoutRenderPanel getRenderPanel() { return renderPanel; } - // Linked controls + /* Linked controls */ SpinnerSliderPaired anchorXPair, anchorYPair, rotationPair, scalePair, zoomPair; - // Token value getters pointing to the mirror token + /* Token value getters pointing to the mirror token */ public double getTokenSizeScale() { return mirrorToken.getSizeScale(); } public double getTokenScaleX() { return mirrorToken.getScaleX(); } public double getTokenScaleY() { return mirrorToken.getScaleY(); } @@ -193,13 +189,12 @@ public void setParentRoot(JRootPane rp) { public boolean getTokenFlippedY() { return mirrorToken.isFlippedY(); } public boolean getTokenFlippedIso() { return mirrorToken.isFlippedIso(); } - // Token value setters pointing to the mirror token + /* Token value setters pointing to the mirror token */ protected void setTokenScaleX(double scale) { mirrorToken.setScaleX(scale);} protected void setTokenScaleY(double scale) { mirrorToken.setScaleY(scale); } protected void setTokenSizeScale(double scale) { mirrorToken.setSizeScale(scale); } protected void setTokenAnchorX(Number x) { mirrorToken.setAnchorX(x.intValue()); } protected void setTokenAnchorY(Number y) { mirrorToken.setAnchorY(y.intValue()); } - protected void setTokenImageRotation(double value) { mirrorToken.setImageRotation(MathUtil.doublePrecision(value, 4)); } protected void setTokenImageRotation(Number value) { mirrorToken.setImageRotation(MathUtil.doublePrecision(value.doubleValue(), 4)); } protected void setTokenFlipIso(Boolean b) { mirrorToken.setFlippedIso(b); @@ -235,7 +230,7 @@ protected void setTokenFlipY(Boolean b) { * There is one spinner/slider for three different scale settings. This sets the value for the * active axis/axes * - * @param value the scale value being set + * @param n (Number) the scale value being set */ protected void setScaleByAxis(Number n) { double value = MathUtil.doublePrecision(n.doubleValue(), 4); @@ -263,8 +258,8 @@ protected Number getScaleByAxis() { } } - // used to determine which scale is being edited - private int scaleAxis = 0; // 0 is XY, 1 is X, 2 is Y + /* used to determine which scale is being edited: 0 is XY, 1 is X, 2 is Y */ + private int scaleAxis = 0; /** * These functions serve to link the scale and zoom sliders and spinners and allow a useful @@ -273,8 +268,8 @@ protected Number getScaleByAxis() { */ Function percentSliderToSpinner = i -> - i.intValue() <= 0 - ? MathUtil.mapToRange(((Number) i).doubleValue(), -200.0, 0.0, 0.0, 1.0).doubleValue() + i <= 0 + ? MathUtil.mapToRange(((Number) i).doubleValue(), -200.0, 0.0, 0.0, 1.0) : MathUtil.mapToRange(((Number) i).doubleValue(), 0.0, 200.0, 1.0, 3.0).doubleValue(); Function percentSpinnerToSlider = @@ -305,36 +300,20 @@ void resetPanelToDefault() { setToken(originalToken, true); } - // Listeners + /* Listeners */ PropertyChangeListener controlListener = - new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - log.debug("controlListener " + evt); - if (evt.getPropertyName().toLowerCase().contains("spinnervalue")) { - iFeelDirty(); - } else if (evt.getPropertyName().toLowerCase().contains("flip")) { - storeFlipDirections(); - } + evt -> { + log.debug("controlListener " + evt); + if (evt.getPropertyName().toLowerCase().contains("spinnervalue")) { + iFeelDirty(); + } else if (evt.getPropertyName().toLowerCase().contains("flip")) { + storeFlipDirections(); } }; - PropertyChangeListener mirrorTokenListener = - new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - log.debug( - "mirrorTokenListener - " - + evt.getPropertyName() - + ":" - + evt.getOldValue() - + ":" - + evt.getNewValue()); - } - }; FocusListener focusListener = new FocusListener() { - // Toggle "Enter" closing the window. + /* Toggle "Enter" closing the window. */ @Override public void focusGained(FocusEvent e) { ((JComponent) e.getComponent()).getRootPane().setDefaultButton(null); @@ -355,14 +334,9 @@ public void itemStateChanged(ItemEvent e) { } }; - protected Token getMirrorToken() { - return mirrorToken; - } - /** Mark the rendering panel in need of repainting */ void iFeelDirty() { Rectangle panelBounds = getRenderPanel().getBounds(); - panelBounds = getRenderPanel().getBounds(); RepaintManager.currentManager(getRenderPanel()) .addDirtyRegion( getRenderPanel(), panelBounds.x, panelBounds.y, panelBounds.width, panelBounds.height); @@ -391,18 +365,16 @@ public void setToken(Token token, boolean useDefaults) { grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid(); isoGrid = grid.isIsometric(); hexGrid = grid.isHex(); - noGrid = GridFactory.getGridType(grid) == GridFactory.NONE; - squareGrid = GridFactory.getGridType(grid) == GridFactory.SQUARE; + noGrid = GridFactory.getGridType(grid).equals(GridFactory.NONE); + squareGrid = GridFactory.getGridType(grid).equals(GridFactory.SQUARE); renderBits = new RenderBits(); gridSize = grid.getSize(); cellHeight = grid.getCellHeight(); cellWidth = grid.getCellWidth(); - this.token = token; - this.originalToken = new Token(this.token, true); // duplicate for resetting purposes - // The mirror token exists so we can write token changes without committing them prior to - // clicking OK - this.mirrorToken = new Token(this.token, false); + this.originalToken = new Token(token, true); // duplicate for resetting purposes + /* The mirror token exists so we can write token changes without committing them prior to clicking OK */ + this.mirrorToken = new Token(token, false); if (useDefaults) { mirrorToken.setImageRotation(0); @@ -412,7 +384,7 @@ public void setToken(Token token, boolean useDefaults) { mirrorToken.setAnchor(0, 0); } - if (mirrorToken.getFootprint(grid) != (TokenFootprint) getSizeCombo().getSelectedItem()) { + if (mirrorToken.getFootprint(grid) != getSizeCombo().getSelectedItem()) { TokenFootprint tmpFP = (TokenFootprint) getSizeCombo().getSelectedItem(); if (tmpFP != null) { mirrorToken.setFootprint(grid, grid.getFootprint(tmpFP.getId())); @@ -428,20 +400,20 @@ public void setToken(Token token, boolean useDefaults) { getAnchorXSlider().setMinimum((int) -Math.ceil(footprintBounds.getWidth())); getAnchorXSlider().setMaximum((int) Math.ceil(footprintBounds.getWidth())); if (isIsoFigure) { - // Allow more vertical travel for iso figures + /* Allow more vertical travel for iso figures */ getAnchorYSlider().setMinimum((int) -Math.ceil(1.4 * footprintBounds.getHeight())); getAnchorYSlider().setMaximum((int) Math.ceil(1.4 * footprintBounds.getHeight())); } else { getAnchorYSlider().setMinimum((int) -Math.ceil(footprintBounds.getHeight())); getAnchorYSlider().setMaximum((int) Math.ceil(footprintBounds.getHeight())); } - // align mouse drag bounds with sliders + /* align mouse drag bounds with sliders */ getRenderPanel().setMaxXoff(getAnchorXSlider().getMaximum()); getRenderPanel().setMaxYoff(getAnchorYSlider().getMaximum()); storeFlipDirections(); - // Assign Suppliers and Consumers to the linked controls and add a PropertyChangeListener + /* Assign Suppliers and Consumers to the linked controls and add a PropertyChangeListener */ anchorXPair.setPropertySetter(this::setTokenAnchorX); anchorXPair.setPropertyGetter(this::getTokenAnchorX); anchorXPair.setPropertyName("AnchorX"); @@ -533,14 +505,11 @@ private void pairControls() { percentSpinnerToSlider, percentSliderToSpinner); scalePair.addVetoableChangeListener( - new VetoableChangeListener() { - @Override - public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { - if (evt.getPropertyName().toLowerCase().contains("value") - && evt.getNewValue().getClass().isAssignableFrom(Double.class) - && ((Number) evt.getNewValue()).doubleValue() < 0.1) { - throw new PropertyVetoException("Minimum scale value reached", evt); - } + evt -> { + if (evt.getPropertyName().toLowerCase().contains("value") + && evt.getNewValue().getClass().isAssignableFrom(Double.class) + && ((Number) evt.getNewValue()).doubleValue() < 0.1) { + throw new PropertyVetoException("Minimum scale value reached", evt); } }); zoomPair = @@ -551,14 +520,11 @@ public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException percentSpinnerToSlider, percentSliderToSpinner); zoomPair.addVetoableChangeListener( - new VetoableChangeListener() { - @Override - public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { - if (evt.getPropertyName().toLowerCase().contains("value") - && evt.getNewValue().getClass().isAssignableFrom(Double.class) - && ((Number) evt.getNewValue()).doubleValue() < 0.34) { - throw new PropertyVetoException("Minimum zoom value reached", evt); - } + evt -> { + if (evt.getPropertyName().toLowerCase().contains("value") + && evt.getNewValue().getClass().isAssignableFrom(Double.class) + && ((Number) evt.getNewValue()).doubleValue() < 0.34) { + throw new PropertyVetoException("Minimum zoom value reached", evt); } }); anchorXPair.addPropertyChangeListener(evt -> iFeelDirty()); @@ -569,14 +535,14 @@ public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException } public void initSpinners() { - // models + /* models */ getAnchorXSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d)); getAnchorYSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d)); getRotationSpinner().setModel(new SpinnerNumberModel(0d, 0d, 360d, 1d)); getScaleSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1)); getZoomSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1)); getZoomSpinner().setVisible(false); - // editors + /* editors */ getAnchorXSpinner().setEditor(new JSpinner.NumberEditor(anchorXSpinner, "0")); getAnchorYSpinner().setEditor(new JSpinner.NumberEditor(anchorYSpinner, "0")); getRotationSpinner().setEditor(new JSpinner.NumberEditor(rotationSpinner, "0.0")); @@ -587,7 +553,7 @@ public void initSpinners() { ((JSpinner.NumberEditor) getRotationSpinner().getEditor()).getTextField().setColumns(3); ((JSpinner.NumberEditor) getScaleSpinner().getEditor()).getTextField().setColumns(4); - // listeners + /* listeners */ ((JSpinner.NumberEditor) getAnchorXSpinner().getEditor()) .getTextField() .addFocusListener(focusListener); @@ -603,7 +569,7 @@ public void initSpinners() { } public void initButtons() { - // icons + /* icons */ createButtonIcons(); scaleButton = new FlatButton(); @@ -612,7 +578,7 @@ public void initButtons() { getScaleButton().setIcon(scaleIcons[scaleAxis]); getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " "); - layoutHelpButton = new FlatButton(); + AbstractButton layoutHelpButton = new FlatButton(); ((FlatButton) layoutHelpButton).setButtonType(FlatButton.ButtonType.help); parent.replaceComponent("layoutTabPanel", "layoutHelpButton", layoutHelpButton); layoutHelpButton.setToolTipText(helpText); @@ -630,37 +596,27 @@ public void mouseExited(MouseEvent e) { private String assembleHelpText() { String rowText = "
%s
%s%s"; String caption = ""; - StringBuilder sb = new StringBuilder(); - sb.append(caption.formatted(I18N.getString("EditTokenDialog.layout.help.caption"))); - sb.append( - rowText.formatted( + return caption.formatted(I18N.getString("EditTokenDialog.layout.help.caption")) + + rowText.formatted( I18N.getString("Mouse.leftDrag"), - I18N.getString("EditTokenDialog.layout.help.moveImage"))); - sb.append( - rowText.formatted( + I18N.getString("EditTokenDialog.layout.help.moveImage")) + + rowText.formatted( I18N.getString("Mouse.rightDrag"), - I18N.getString("EditTokenDialog.layout.help.moveView"))); - sb.append( - rowText.formatted( + I18N.getString("EditTokenDialog.layout.help.moveView")) + + rowText.formatted( I18N.getString("Mouse.leftDoubleClick"), - I18N.getString("EditTokenDialog.layout.help.reset"))); - sb.append( - rowText.formatted( + I18N.getString("EditTokenDialog.layout.help.reset")) + + rowText.formatted( I18N.getString("Mouse.rightDoubleClick"), - I18N.getString("EditTokenDialog.layout.help.resetDefaults"))); - sb.append( - rowText.formatted( - I18N.getString("Mouse.wheel"), - I18N.getString("EditTokenDialog.layout.help.scaleImage"))); - sb.append( - rowText.formatted( + I18N.getString("EditTokenDialog.layout.help.resetDefaults")) + + rowText.formatted( + I18N.getString("Mouse.wheel"), I18N.getString("EditTokenDialog.layout.help.scaleImage")) + + rowText.formatted( I18N.getString("Mouse.shiftWheel"), - I18N.getString("EditTokenDialog.layout.help.rotateImage"))); - sb.append( - rowText.formatted( + I18N.getString("EditTokenDialog.layout.help.rotateImage")) + + rowText.formatted( I18N.getString("Mouse.ctrlWheel"), - I18N.getString("EditTokenDialog.layout.help.zoomView"))); - return sb.toString(); + I18N.getString("EditTokenDialog.layout.help.zoomView")); } private void showHelp() { @@ -712,8 +668,8 @@ protected void paintComponent(Graphics g) { g2d.dispose(); } } - Dictionary offSetYLabels = new Hashtable(); - Dictionary offSetXLabels = new Hashtable(); + Dictionary offSetYLabels = new Hashtable<>(); + Dictionary offSetXLabels = new Hashtable<>(); for (int i = -1200; i <= 1200; i += 50) { JLabel label1 = new JLabel(String.valueOf(i)); offSetYLabels.put(i, label1); @@ -726,7 +682,7 @@ protected void paintComponent(Graphics g) { getAnchorXSlider().setLabelTable(offSetXLabels); DecimalFormat df = new DecimalFormat("##0%"); - Dictionary pctLabels = new Hashtable(); + Dictionary pctLabels = new Hashtable<>(); pctLabels.put(-200, new JLabel(df.format(0))); pctLabels.put(-100, new JLabel(df.format(0.5))); pctLabels.put(0, new JLabel(df.format(1))); @@ -772,7 +728,7 @@ private void setControlValues() { public static ImageIcon makeSVGIcon(int size, String svg) { InputStream stream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); FlatSVGIcon.ColorFilter colorFilter = new FlatSVGIcon.ColorFilter(); - // Convert a the original color into another one supposedly + /* Convert a the original color into another one supposedly */ colorFilter.add(Color.BLACK, UI_DEFAULTS.getColor(FlatIconColors.OBJECTS_BLACK_TEXT)); colorFilter.add(Color.WHITE, UI_DEFAULTS.getColor(FlatIconColors.ACTIONS_BLUE_DARK)); try { @@ -799,15 +755,15 @@ public void actionPerformed(ActionEvent e) { getScaleButton().setIcon(scaleIcons[scaleAxis]); switch (scaleAxis) { case 0 -> { - getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenSizeScale()).intValue()); + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenSizeScale())); getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " "); } case 1 -> { - getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleX()).intValue()); + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleX())); getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-X"); } case 2 -> { - getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleY()).intValue()); + getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleY())); getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-Y"); } } @@ -841,7 +797,7 @@ void init() { viewOffset = getRenderPanel().getViewOffset(); centrePoint = new Point2D.Double( - size.width / 2 + viewOffset.getX(), size.height / 2 + viewOffset.getY()); + size.width / 2d + viewOffset.getX(), size.height / 2d + viewOffset.getY()); if (zoomFactor != getRenderPanel().getZoomFactor()) { zoomFactor = getRenderPanel().getZoomFactor(); @@ -850,7 +806,7 @@ void init() { } if (tokenImage == null) { - // just to avoid Div/0 if called before image loaded + /* just to avoid Div/0 if called before image loaded */ tokenImage = new BufferedImage( (int) footprintBounds.getWidth(), @@ -859,13 +815,12 @@ void init() { } if (isIsoFigure) { - // If figure we need to calculate an additional offset for the token height outside - // footprint + /* If figure we calculate an additional offset for token height outside footprint */ double imageFitRatio = footprintBounds.getWidth() / tokenImage.getWidth(); // scale for width if (tokenImage.getHeight() * footprintBounds.getWidth() / tokenImage.getWidth() > 2 * footprintBounds.getHeight()) { - // if this results in being more than twice the footprint height, use height instead + /* if this results in being more than twice the footprint height, use height instead */ imageFitRatio = footprintBounds.getHeight() * 2 / tokenImage.getHeight(); } double th = tokenImage.getHeight() * imageFitRatio; @@ -887,7 +842,7 @@ private void setStrokeArrays() { boolean mapValues = constrainedZoom != 1f; for (int i = 0; i < strokeModels.length; i++) { BasicStroke model = strokeModels[i]; - float useWidth = 0; + float useWidth; if (mapValues) { useWidth = MathUtil.mapToRange( @@ -963,8 +918,7 @@ private void setStrokeArrays() { protected BufferedImage getFlippedImage(BufferedImage bi) { log.debug("getFlippedImage - flipStates: " + flipStates); int direction = - 0 - + (flipStates.contains(FlipState.HORIZONTAL) ? 1 : 0) + (flipStates.contains(FlipState.HORIZONTAL) ? 1 : 0) + (flipStates.contains(FlipState.VERTICAL) ? 2 : 0); if (direction != 0) { bi = ImageUtil.flipCartesian(bi, direction); @@ -975,17 +929,6 @@ protected BufferedImage getFlippedImage(BufferedImage bi) { return bi; } - /** - * Rotates a buffered image by the provided angle in degrees - * - * @param bi Image to transform - * @param angle Angle in degrees - * @return Rotated bufferedImage - */ - protected BufferedImage getRotatedImage(BufferedImage bi, Number angle) { - return ImageUtil.rotateImage(bi, angle.doubleValue()); - } - private Shape createGridShape(boolean trueSize) { return GraphicsUtil.createGridShape( GridFactory.getGridType(grid), (trueSize ? grid.getSize() : grid.getSize() - 8)); @@ -1033,12 +976,12 @@ protected void paintCentreMark(Graphics g) { * ring of a different colour at the radius associated with the footprint scale. Also paints * some radial lines to assist with alignment * - * @param g - * @param zoomFactor + * @param g graphics object + * @param zoomFactor zoom level applied to view */ void paintRings(Graphics g, double zoomFactor) { Graphics2D g2d = (Graphics2D) g.create(); - // used with no grid. A set of rings and radial lines. + /* used with no grid. A set of rings and radial lines. */ TokenFootprint fp = mirrorToken.getFootprint(grid); Rectangle2D fpCellBounds = fp.getBounds(grid, ORIGIN); fpCellBounds = @@ -1058,10 +1001,10 @@ void paintRings(Graphics g, double zoomFactor) { Line2D lineLong = new Line2D.Double(cx + currentRadius / 2d, cy, maxRadius, cy); Line2D lineShort = new Line2D.Double(cx + gap * 2d, cy, maxRadius, cy); - // draw radial lines + /* draw radial lines */ for (double i = 0; i < 24; i++) { if (i % 6 == 0) { - continue; // skip cardinal lines + continue; /* skip cardinal lines */ } paintShapeOutLine( g2d, @@ -1071,7 +1014,7 @@ void paintRings(Graphics g, double zoomFactor) { true); } - // draw rings + /* draw rings */ while (currentRadius < maxRadius) { Ellipse2D e = new Ellipse2D.Double( @@ -1091,16 +1034,13 @@ void paintRings(Graphics g, double zoomFactor) { /** * Horizontal and vertical lines oriented on cx/cy * - * @param g + * @param g graphics object * @param rotation used to draw lines on rotated images - * @param cx vertical line position - * @param cy horizontal line position * @param solid Draw as solid else dashed * @param colourSet1 Use colour set 1 else 2 */ void paintCentreLines(Graphics g, double rotation, boolean solid, boolean colourSet1) { - Graphics old = g; - // create crosshair with a central gap + /* create cross-hair with a central gap */ double cx = centrePoint.getX(); double cy = centrePoint.getY(); Rectangle2D r = viewBounds; @@ -1132,7 +1072,6 @@ void paintCentreLines(Graphics g, double rotation, boolean solid, boolean colour s = lines; } paintShapeOutLine(g, s, solid, colourSet1); - g = old; } /* @@ -1158,8 +1097,6 @@ void paintShapeOutLine(Graphics g, Shape shp, boolean solid, boolean colourSet1) Graphics2D g2d = (Graphics2D) g.create(); Composite oldAc = g2d.getComposite(); AlphaComposite ac; - BasicStroke[] strokes = solid ? solidStrokes : dashedStrokes; - for (int i = 0; i < 4; i++) { switch (i) { case 0, 1 -> g2d.setColor(colourSet1 ? colours[2] : colours[3]); @@ -1209,7 +1146,7 @@ void paintFootprint(Graphics g, double zoomFactor) { double yCorrection = isIsoFigure ? footprintScale * footprintBounds.getHeight() / 2d : 0; Shape scaledOutline, scaledFill; - // for drawing sub-cell-sizes + /* for drawing sub-cell-sizes */ if (footprintScale < 1d) { scaledOutline = AffineTransform.getScaleInstance(footprintScale, footprintScale) @@ -1284,7 +1221,6 @@ void paintExtraGuides(Graphics g) { void paintToken(Graphics g, boolean translucent) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHints(RENDERING_HINTS); - AffineTransform oldXform = g2d.getTransform(); if (centreMark == null) { createCentreMark(); } From 950b5a4034b5e1187a37450ede343442caa6b9c1 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:46:28 +0800 Subject: [PATCH 12/24] Code cleanup --- .../java/net/rptools/lib/image/ImageUtil.java | 57 ++++++------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index a2950b0cc7..f8e08a2015 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -127,19 +127,17 @@ public static BufferedImage getScaledTokenImage(BufferedImage bi, Token token, Z public static BufferedImage getScaledTokenImage( BufferedImage img, Token token, Grid grid, double zoom) { TokenFootprint footprint = token.getFootprint(grid); - Rectangle2D footprintBounds = footprint.getBounds(grid, new CellPoint(0, 0)); - double zoomS = zoom; - double fpS = - footprint - .getScale(); // except gridless, this should be 1 for footprints larger than the grid + Rectangle2D footprintBounds = footprint.getBounds(grid); // , new CellPoint(0, 0)); + // except gridless, this should be 1 for footprints larger than the grid + double fpS = footprint.getScale(); // size double fpW, fpH; - // multiply by zoom level to prevent mutliple scaling ops which lose definition - if (grid.equals(GridFactory.NONE)) { - fpW = fpH = grid.getSize() * fpS * zoomS; // all gridless are relative to the grid size + // multiply by zoom level to prevent multiple scaling ops which lose definition, i.e. scale once + if (GridFactory.getGridType(grid).equals(GridFactory.NONE)) { + fpW = fpH = grid.getSize() * fpS * zoom; // all gridless are relative to the grid size } else { - fpW = footprintBounds.getWidth() * fpS * zoomS; - fpH = footprintBounds.getHeight() * fpS * zoomS; + fpW = footprintBounds.getWidth() * fpS * zoom; + fpH = footprintBounds.getHeight() * fpS * zoom; } double imgW = img.getWidth(); @@ -501,9 +499,12 @@ public static int pickBestTransparency(BufferedImage image) { } public static double getIsoFigureHeightOffset(Token token, Rectangle2D footprintBounds) { - double scale = getIsoFigureScaleFactor(token, footprintBounds); - double th = token.getHeight() * scale; - return footprintBounds.getHeight() - th; + if (token.getShape().equals(Token.TokenShape.FIGURE) && !token.isFlippedIso()) { + double imageFitRatio = getIsoFigureScaleFactor(token, footprintBounds); + double th = token.getHeight() * imageFitRatio; + return footprintBounds.getHeight() - th; + } + return 0; } /** @@ -519,41 +520,17 @@ public static double getIsoFigureScaleFactor(Token token, Rectangle2D footprintB footprintBounds.getHeight() * 2 / token.getHeight()); } - /** - * Get the offset values required to align an image within specified bounds - * - * @param imgSize Dimension - * @param footprintBounds Rectangle - * @return int array of length 2 [x,y] - */ - public static double[] getImageAlignmentOffsets(BufferedImage image, Rectangle footprintBounds) { - double[] offsets = new double[2]; - offsets[0] = - image.getWidth() < footprintBounds.width - ? (footprintBounds.width - image.getWidth()) / 2 - : image.getWidth() > footprintBounds.width - ? -(image.getWidth() - footprintBounds.width) / 2 - : 0; - offsets[1] = - image.getHeight() < footprintBounds.height - ? (footprintBounds.height - image.getHeight()) / 2 - : image.getHeight() > footprintBounds.height - ? -(image.getHeight() - footprintBounds.height) / 2 - : 0; - return offsets; - } - /** * Gets the token image; applies flipping, scaling, and image rotation, but not facing. * - * @param token - * @param zr + * @param token Token + * @param zr ZoneRenderer * @return modified image */ public static BufferedImage getTokenRenderImage(Token token, ZoneRenderer zr) { BufferedImage image = getTokenImage(token, zr); - int flipDirection = 0 + (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); + int flipDirection = (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); image = flipCartesian(image, flipDirection); if (token.isFlippedIso() && zr.getZone().getGrid().isIsometric()) { image = flipIsometric(image, true); From 0786ea02e15a91a5eecf9e1bd59b56f41845b99a Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:47:26 +0800 Subject: [PATCH 13/24] Refactoring --- src/main/java/net/rptools/maptool/client/tool/StampTool.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 4995f79de4..a7af2a0b4e 100644 --- a/src/main/java/net/rptools/maptool/client/tool/StampTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/StampTool.java @@ -1346,8 +1346,7 @@ public void dragTo(int mouseX, int mouseY, boolean lockAspectRatio, boolean snap // For snap-to-grid tokens (except background stamps) we anchor at the center of the token. final var isSnapToGridAndAnchoredAtCenter = - tokenBeingResized.isSnapToGrid() - && tokenBeingResized.getLayer().anchorSnapToGridAtCenter(); + tokenBeingResized.isSnapToGrid() && tokenBeingResized.getLayer().isSnapToGridAtCenter(); final var snapToGridMultiplier = isSnapToGridAndAnchoredAtCenter ? 2 : 1; var widthIncrease = adjustment.x * snapToGridMultiplier; var heightIncrease = adjustment.y * snapToGridMultiplier; From 789928eb3bf097d850a9d0e562d55aa0fabd6f78 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:47:55 +0800 Subject: [PATCH 14/24] Refactoring --- .../rptools/maptool/client/ui/zone/renderer/TokenLocation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java index 1f95f76940..dd4b65b8cd 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java @@ -20,7 +20,7 @@ import net.rptools.lib.CodeTimer; import net.rptools.maptool.model.Token; -class TokenLocation { +public class TokenLocation { private final ZoneRenderer renderer; public Area bounds; From 0cb23f97598ea8d656d4647f753b49cf45f8ff6f Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:49:36 +0800 Subject: [PATCH 15/24] Refactoring --- .../java/net/rptools/maptool/model/Token.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 6a85d27c6e..c730c6da46 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -46,6 +46,7 @@ import net.rptools.lib.MD5Key; import net.rptools.lib.image.ImageUtil; import net.rptools.lib.transferable.TokenTransferData; +import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.AppUtil; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.MapToolVariableResolver; @@ -287,7 +288,8 @@ public enum Update { MapTool.getCampaign().getCampaignProperties().getDefaultTokenPropertyType(); private Integer haloColorValue; - private transient Color haloColor; + private transient Color haloColor = + new Color(ImageUtil.negativeColourInt(AppPreferences.defaultGridColor.getDefault().getRGB())); private Integer visionOverlayColorValue; private transient Color visionOverlayColor; @@ -1516,13 +1518,10 @@ public Rectangle getBounds(Zone zone) { footprintBounds.x = getX(); footprintBounds.y = getY(); } else { - if (getLayer().anchorSnapToGridAtCenter()) { + if (getLayer().isSnapToGridAtCenter()) { // Center it on the footprint - footprintBounds.x -= (w - footprintBounds.width) / 2; - footprintBounds.y -= (h - footprintBounds.height) / 2; - } else { - // footprintBounds.x -= zone.getGrid().getSize()/2; - // footprintBounds.y -= zone.getGrid().getSize()/2; + footprintBounds.x -= (int) ((w - footprintBounds.width) / 2d); + footprintBounds.y -= (int) ((h - footprintBounds.height) / 2d); } } footprintBounds.width = (int) w; // perhaps make this a double @@ -1548,7 +1547,7 @@ public ZonePoint getDragAnchor(Zone zone) { Grid grid = zone.getGrid(); int dragAnchorX, dragAnchorY; if (isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { - if (!getLayer().isStampLayer() || !getLayer().anchorSnapToGridAtCenter() || isSnapToScale()) { + if (!getLayer().isStampLayer() || !getLayer().isSnapToGridAtCenter() || isSnapToScale()) { Point2D.Double centerOffset = grid.getCenterOffset(); dragAnchorX = getX() + (int) centerOffset.x; dragAnchorY = getY() + (int) centerOffset.y; @@ -1612,7 +1611,7 @@ public ZonePoint getSnappedPoint(Zone zone) { Point2D.Double offset = getSnapToUnsnapOffset(zone); double newX = getX() + offset.x; double newY = getY() + offset.y; - if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().anchorSnapToGridAtCenter()) { + if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().isSnapToGridAtCenter()) { return grid.convert( grid.convert(new ZonePoint((int) Math.ceil(newX), (int) Math.ceil(newY)))); } else { @@ -1645,13 +1644,13 @@ private Point2D.Double getSnapToUnsnapOffset(Zone zone) { double offsetX, offsetY; Rectangle tokenBounds = getBounds(zone); Grid grid = zone.getGrid(); - if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().anchorSnapToGridAtCenter()) { - if (!getLayer().anchorSnapToGridAtCenter() || isSnapToScale()) { + if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().isSnapToGridAtCenter()) { + if (!getLayer().isSnapToGridAtCenter() || isSnapToScale()) { TokenFootprint footprint = getFootprint(grid); Rectangle footprintBounds = footprint.getBounds(grid); double footprintOffsetX = 0; double footprintOffsetY = 0; - if (getLayer().anchorSnapToGridAtCenter()) { + if (getLayer().isSnapToGridAtCenter()) { // Non-background tokens can have an offset from top left corner footprintOffsetX = tokenBounds.width - footprintBounds.width; footprintOffsetY = tokenBounds.height - footprintBounds.height; From 408ef52933afb8805cf960d6968e54d693676b4b Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:50:27 +0800 Subject: [PATCH 16/24] Refactoring --- src/main/java/net/rptools/maptool/model/Zone.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rptools/maptool/model/Zone.java b/src/main/java/net/rptools/maptool/model/Zone.java index 487195b401..bd5ca62486 100644 --- a/src/main/java/net/rptools/maptool/model/Zone.java +++ b/src/main/java/net/rptools/maptool/model/Zone.java @@ -251,7 +251,7 @@ public boolean supportsVision() { * @return {@code true} if the {@code Token} instances on the layer should be resized relative * to their center rather than their corner. */ - public boolean anchorSnapToGridAtCenter() { + public boolean isSnapToGridAtCenter() { return this != BACKGROUND; } From 9e46740f9e75563fcb9d8d176215b4a420be2d42 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:59:28 +0800 Subject: [PATCH 17/24] Refactoring Token drawing moved to TokenRenderer Facing-arrow painting moved to FacingArrowRenderer Some functionality moved to ImageUtils --- .../client/ui/zone/renderer/ZoneRenderer.java | 272 ++---------------- .../renderer/tokenRender/TokenRenderer.java | 222 ++++++++++++++ 2 files changed, 253 insertions(+), 241 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java 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 3f80194786..b73557b822 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 @@ -38,6 +38,7 @@ import javax.swing.*; import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; +import net.rptools.lib.image.ImageUtil; import net.rptools.maptool.client.*; import net.rptools.maptool.client.events.ZoneLoaded; import net.rptools.maptool.client.functions.TokenMoveFunctions; @@ -57,6 +58,7 @@ 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.renderer.tokenRender.TokenRenderer; import net.rptools.maptool.client.walker.ZoneWalker; import net.rptools.maptool.events.MapToolEventBus; import net.rptools.maptool.language.I18N; @@ -148,6 +150,7 @@ public class ZoneRenderer extends JComponent implements DropTargetListener { private final ZoneCompositor compositor; private final GridRenderer gridRenderer; private final HaloRenderer haloRenderer; + private final TokenRenderer tokenRenderer; private final LightsRenderer lightsRenderer; private final DarknessRenderer darknessRenderer; private final LumensRenderer lumensRenderer; @@ -176,6 +179,7 @@ public ZoneRenderer(Zone zone) { this.compositor = new ZoneCompositor(); this.gridRenderer = new GridRenderer(); this.haloRenderer = new HaloRenderer(); + this.tokenRenderer = new TokenRenderer(); this.lightsRenderer = new LightsRenderer(renderHelper, zone, zoneView); this.darknessRenderer = new DarknessRenderer(renderHelper, zoneView); this.lumensRenderer = new LumensRenderer(renderHelper, zone, zoneView); @@ -285,7 +289,7 @@ public void setZoneScale(Scale scale) { scale.addPropertyChangeListener( evt -> { if (Scale.PROPERTY_SCALE.equals(evt.getPropertyName())) { - tokenLocationCache.clear(); + clearZoomDependantCaches(); } if (Scale.PROPERTY_OFFSET.equals(evt.getPropertyName())) { // flushFog = true; @@ -892,6 +896,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) { if (!compositor.isInitialised()) compositor.setRenderer(this); if (!gridRenderer.isInitialised()) gridRenderer.setRenderer(this); if (!haloRenderer.isInitialised()) haloRenderer.setRenderer(this); + if (!tokenRenderer.isInitialised()) tokenRenderer.setRenderer(this); Rectangle viewRect = new Rectangle(getSize().width, getSize().height); @@ -1455,19 +1460,8 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set footprintBounds = token.getBounds(zone); } // Draw token - double iso_ho = 0; + double iso_ho = ImageUtil.getIsoFigureHeightOffset(token, footprintBounds) * getScale(); Dimension imgSize = new Dimension(workImage.getWidth(), workImage.getHeight()); - if (token.getShape() == TokenShape.FIGURE) { - double th = token.getHeight() * (double) footprintBounds.width / token.getWidth(); - iso_ho = footprintBounds.height - th; - footprintBounds = - new Rectangle( - footprintBounds.x, - footprintBounds.y - (int) iso_ho, - footprintBounds.width, - (int) th); - iso_ho = iso_ho * getScale(); - } SwingUtil.constrainTo(imgSize, footprintBounds.width, footprintBounds.height); int offsetx = 0; @@ -2015,60 +2009,6 @@ private List getTokenLocations(Zone.Layer layer) { return tokenLocationMap.computeIfAbsent(layer, k -> new LinkedList<>()); } - // TODO: I don't like this hardwiring - protected Shape getFigureFacingArrow(int angle, int size) { - int base = (int) (size * .75); - int width = (int) (size * .35); - - facingArrow = new GeneralPath(); - facingArrow.moveTo(base, -width); - facingArrow.lineTo(size, 0); - facingArrow.lineTo(base, width); - facingArrow.lineTo(base, -width); - - GeneralPath gp = - (GeneralPath) - facingArrow.createTransformedShape( - AffineTransform.getRotateInstance(-Math.toRadians(angle))); - return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale() / 2)); - } - - // TODO: I don't like this hardwiring - protected Shape getCircleFacingArrow(int angle, int size) { - int base = (int) (size * .75); - int width = (int) (size * .35); - - facingArrow = new GeneralPath(); - facingArrow.moveTo(base, -width); - facingArrow.lineTo(size, 0); - facingArrow.lineTo(base, width); - facingArrow.lineTo(base, -width); - - GeneralPath gp = - (GeneralPath) - facingArrow.createTransformedShape( - AffineTransform.getRotateInstance(-Math.toRadians(angle))); - return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale())); - } - - // TODO: I don't like this hardwiring - protected Shape getSquareFacingArrow(int angle, int size) { - int base = (int) (size * .75); - int width = (int) (size * .35); - - facingArrow = new GeneralPath(); - facingArrow.moveTo(0, 0); - facingArrow.lineTo(-(size - base), -width); - facingArrow.lineTo(-(size - base), width); - facingArrow.lineTo(0, 0); - - GeneralPath gp = - (GeneralPath) - facingArrow.createTransformedShape( - AffineTransform.getRotateInstance(-Math.toRadians(angle))); - return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale())); - } - protected void renderTokens(Graphics2D g, List tokenList, PlayerView view) { renderTokens(g, tokenList, view, false); } @@ -2118,7 +2058,6 @@ protected void renderTokens( // Is that because the rendering pipeline thinks they've already been drawn so isn't forced to // re-render them? So how does this cache get filled then? It's not part of the campaign // state... - // tokenLocationCache.clear(); List tokenPostProcessing = new ArrayList(tokenList.size()); for (Token token : tokenList) { @@ -2165,12 +2104,6 @@ protected void renderTokens( double scaledWidth = (footprintBounds.width * scale); double scaledHeight = (footprintBounds.height * scale); - // if (!token.isStamp()) { - // // Fit inside the grid - // scaledWidth --; - // scaledHeight --; - // } - ScreenPoint tokenScreenLocation = ScreenPoint.fromZonePoint(this, footprintBounds.x, footprintBounds.y); timer.stop("tokenlist-1c"); @@ -2186,14 +2119,8 @@ protected void renderTokens( double sx = scaledWidth / 2 + x - (token.getAnchor().x * scale); double sy = scaledHeight / 2 + y - (token.getAnchor().y * scale); tokenBounds.transform( - AffineTransform.getRotateInstance( - Math.toRadians(token.getFacingInDegrees()), sx, sy)); // facing - // defaults - // to - // down, - // or - // -90 - // degrees + AffineTransform.getRotateInstance(Math.toRadians(token.getFacingInDegrees()), sx, sy)); + // facing defaults to down or -90 degrees } timer.stop("tokenlist-1d"); @@ -2228,7 +2155,6 @@ protected void renderTokens( } // Markers timer.start("renderTokens:Markers"); - // System.out.println("Token " + token.getName() + " is a marker? " + token.isMarker()); if (token.isMarker() && canSeeMarker(token)) { markerLocationList.add(location); } @@ -2237,13 +2163,10 @@ protected void renderTokens( // Stacking check if (calculateStacks) { timer.start("tokenStack"); - // System.out.println(token.getName() + " - " + location.boundsCache); Set tokenStackSet = null; for (TokenLocation currLocation : getTokenLocations(Zone.Layer.TOKEN)) { // Are we covering anyone ? - // System.out.println("\t" + currLocation.token.getName() + " - " + - // location.boundsCache.contains(currLocation.boundsCache)); if (location.boundsCache.contains(currLocation.boundsCache)) { if (tokenStackSet == null) { tokenStackSet = new HashSet(); @@ -2384,11 +2307,6 @@ protected void renderTokens( // Rotated if (token.hasFacing() && token.getShape() == Token.TokenShape.TOP_DOWN) { - // Jamz: Test, rotate on NW corner - // at.rotate(Math.toRadians(token.getFacingInDegrees()), (token.getAnchor().x * scale) - - // offsetx, - // (token.getAnchor().y * scale) - offsety); - at.rotate( Math.toRadians(token.getFacingInDegrees()), location.scaledWidth / 2 - (token.getAnchor().x * scale) - offsetx, @@ -2416,7 +2334,10 @@ protected void renderTokens( // Calculate alpha Transparency from token and use opacity for indicating that token is moving float opacity = token.getOpacity(); if (isTokenMoving(token)) opacity = opacity / 2.0f; - + Map renderInfo = new HashMap<>(); + renderInfo.put(TokenRenderer.OPACITY, opacity); + renderInfo.put(TokenRenderer.GRAPHICS, tokenG); + renderInfo.put(TokenRenderer.LOCATION, location); // Finally render the token image timer.start("tokenlist-7"); if (!isGMView && zoneView.isUsingVision() && (token.getShape() == Token.TokenShape.FIGURE)) { @@ -2425,24 +2346,14 @@ protected void renderTokens( // the cell intersects visible area so if (zone.getGrid().checkCenterRegion(cb.getBounds(), visibleScreenArea)) { // if we can see the centre, draw the whole token - Composite oldComposite = tokenG.getComposite(); - if (opacity < 1.0f) { - tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - } - tokenG.drawImage(workImage, at, this); - tokenG.setComposite(oldComposite); - // g.draw(cb); // debugging + tokenRenderer.renderToken(token, renderInfo); + } else { // else draw the clipped token Area cellArea = new Area(visibleScreenArea); cellArea.intersect(cb); - tokenG.setClip(cellArea); - Composite oldComposite = tokenG.getComposite(); - if (opacity < 1.0f) { - tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - } - tokenG.drawImage(workImage, at, this); - tokenG.setComposite(oldComposite); + renderInfo.put(TokenRenderer.CLIP, cellArea); + tokenRenderer.renderToken(token, renderInfo); } } } else if (!isGMView && zoneView.isUsingVision() && token.isAlwaysVisible()) { @@ -2452,142 +2363,27 @@ protected void renderTokens( // if we can see a portion of the stamp/token, draw the whole thing, defaults to 2/9ths if (zone.getGrid() .checkRegion(cb.getBounds(), visibleScreenArea, token.getAlwaysVisibleTolerance())) { - Composite oldComposite = tokenG.getComposite(); - if (opacity < 1.0f) { - tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - } - tokenG.drawImage(workImage, at, this); - tokenG.setComposite(oldComposite); + tokenRenderer.renderToken(token, renderInfo); } else { // else draw the clipped stamp/token // This will only show the part of the token that does not have VBL on it // as any VBL on the token will block LOS, affecting the clipping. Area cellArea = new Area(visibleScreenArea); cellArea.intersect(cb); - tokenG.setClip(cellArea); - Composite oldComposite = tokenG.getComposite(); - if (opacity < 1.0f) { - tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - } - tokenG.drawImage(workImage, at, this); - tokenG.setComposite(oldComposite); + renderInfo.put(TokenRenderer.CLIP, cellArea); + tokenRenderer.renderToken(token, renderInfo); } } } else { // fallthrough normal token rendered against visible area - Composite oldComposite = tokenG.getComposite(); - if (opacity < 1.0f) { - tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); - } - tokenG.drawImage(workImage, at, this); - tokenG.setComposite(oldComposite); + tokenRenderer.renderToken(token, renderInfo); } timer.stop("tokenlist-7"); timer.start("tokenlist-8"); - // Halo (SQUARE) - // XXX Why are square halos drawn separately?! - /* - * if (token.hasHalo() && token.getShape() == Token.TokenShape.SQUARE) { Stroke oldStroke = g.getStroke(); clippedG.setStroke(new BasicStroke(AppPreferences.getHaloLineWidth())); - * clippedG.setColor(token.getHaloColor()); clippedG.draw(new Rectangle2D.Double(location.x, location.y, location.scaledWidth, location.scaledHeight)); clippedG.setStroke(oldStroke); } - */ - - // Facing ? - // TODO: Optimize this by doing it once per token per facing + // Facing if (token.hasFacing()) { - Token.TokenShape tokenType = token.getShape(); - switch (tokenType) { - case FIGURE: - if (token.getHasImageTable() && !AppPreferences.forceFacingArrow.get()) { - break; - } - Shape arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2); - - if (!zone.getGrid().isIsometric()) { - arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2); - } - - double fx = location.x + location.scaledWidth / 2; - double fy = location.y + location.scaledHeight / 2; - - tokenG.translate(fx, fy); - if (token.getFacing() < 0) { - tokenG.setColor(Color.yellow); - } else { - tokenG.setColor(ZoneRendererConstants.TRANSLUCENT_YELLOW); - } - tokenG.fill(arrow); - tokenG.setColor(Color.darkGray); - tokenG.draw(arrow); - tokenG.translate(-fx, -fy); - break; - case TOP_DOWN: - if (!AppPreferences.forceFacingArrow.get()) { - break; - } - case CIRCLE: - arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2); - if (zone.getGrid().isIsometric()) { - arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2); - } - - double cx = location.x + location.scaledWidth / 2; - double cy = location.y + location.scaledHeight / 2; - - tokenG.translate(cx, cy); - tokenG.setColor(Color.yellow); - tokenG.fill(arrow); - tokenG.setColor(Color.darkGray); - tokenG.draw(arrow); - tokenG.translate(-cx, -cy); - break; - case SQUARE: - if (zone.getGrid().isIsometric()) { - arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2); - cx = location.x + location.scaledWidth / 2; - cy = location.y + location.scaledHeight / 2; - } else { - int facing = token.getFacing(); - while (facing < 0) { - facing += 360; - } // TODO: this should really be done in Token.setFacing() but I didn't want to take - // the chance - // of breaking something, so change this when it's safe to break stuff - facing %= 360; - arrow = getSquareFacingArrow(facing, footprintBounds.width / 2); - - cx = location.x + location.scaledWidth / 2; - cy = location.y + location.scaledHeight / 2; - - // Find the edge of the image - // TODO: Man, this is horrible, there's gotta be a better way to do this - double xp = location.scaledWidth / 2; - double yp = location.scaledHeight / 2; - if (facing >= 45 && facing <= 135 || facing >= 225 && facing <= 315) { - xp = (int) (yp / Math.tan(Math.toRadians(facing))); - if (facing > 180) { - xp = -xp; - yp = -yp; - } - } else { - yp = (int) (xp * Math.tan(Math.toRadians(facing))); - if (facing > 90 && facing < 270) { - xp = -xp; - yp = -yp; - } - } - cx += xp; - cy -= yp; - } - - tokenG.translate(cx, cy); - tokenG.setColor(Color.yellow); - tokenG.fill(arrow); - tokenG.setColor(Color.darkGray); - tokenG.draw(arrow); - tokenG.translate(-cx, -cy); - break; - } + tokenRenderer.paintFacingArrow(tokenG, token, footprintBounds, location); } timer.stop("tokenlist-8"); @@ -2653,7 +2449,7 @@ protected void renderTokens( } overlay.paintOverlay(locg, token, bounds, barValue); - } // endfor + } locg.dispose(); timer.stop("tokenlist-10"); @@ -2664,13 +2460,6 @@ protected void renderTokens( tokenPostProcessing.add(token); } timer.stop("tokenlist-11"); - - // DEBUGGING - // ScreenPoint tmpsp = ScreenPoint.fromZonePoint(this, new ZonePoint(token.getX(), - // token.getY())); - // g.setColor(Color.red); - // g.drawLine(tmpsp.x, 0, tmpsp.x, getSize().height); - // g.drawLine(0, tmpsp.y, getSize().width, tmpsp.y); } timer.start("tokenlist-12"); boolean useIF = MapTool.getServerPolicy().isUseIndividualFOW(); @@ -2844,10 +2633,6 @@ protected void renderTokens( } // Markers - // for (TokenLocation location : getMarkerLocations()) { - // BufferedImage stackImage = AppStyle.markerImage; - // g.drawImage(stackImage, location.bounds.getBounds().x, location.bounds.getBounds().y, null); - // } if (clippedG != g) { clippedG.dispose(); @@ -3142,13 +2927,18 @@ public void setScale(double scale) { /* * MCL: I think it is correct to clear these caches (if not more). */ - tokenLocationCache.clear(); - invalidateCurrentViewCache(); + clearZoomDependantCaches(); zoneScale.zoomScale(getWidth() / 2, getHeight() / 2, scale); MapTool.getFrame().getZoomStatusBar().update(); } } + private void clearZoomDependantCaches() { + tokenLocationCache.clear(); + invalidateCurrentViewCache(); + tokenRenderer.zoomChanged(); + } + public double getScale() { return zoneScale.getScale(); } 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 new file mode 100644 index 0000000000..43fad8e1c9 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java @@ -0,0 +1,222 @@ +/* + * 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 com.google.common.eventbus.Subscribe; +import java.awt.*; +import java.awt.geom.*; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import net.rptools.lib.CodeTimer; +import net.rptools.lib.image.ImageUtil; +import net.rptools.maptool.client.ui.zone.renderer.TokenLocation; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.events.MapToolEventBus; +import net.rptools.maptool.model.*; +import net.rptools.maptool.model.zones.GridChanged; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +public class TokenRenderer { + public static final String GRAPHICS = "graphics"; + public static final String LOCATION = "location"; + public static final String OPACITY = "opacity"; + public static final String CLIP = "clip"; + private static final Logger log = LogManager.getLogger(TokenRenderer.class); + private final CodeTimer timer; + private final Map renderImageMap = new HashMap<>(); + private final Map tokenStateMap = new HashMap<>(); + private Graphics2D g2d; + private TokenLocation location; + private Token token; + private float opacity = 1f; + private Area clip = null; + private ZoneRenderer renderer; + private Zone zone; + private Grid grid; + private double scale; + private boolean isSquare = false; + private boolean isoFigure = false; + private boolean canSpin = false; + private BufferedImage renderImage; + private boolean initialised = false; + public FacingArrowRenderer facingArrowRenderer; + + public TokenRenderer() { + facingArrowRenderer = new FacingArrowRenderer(); + new MapToolEventBus().getMainEventBus().register(this); + timer = CodeTimer.get(); + } + + public boolean isInitialised() { + return initialised; + } + + public void setRenderer(ZoneRenderer zoneRenderer) { + timer.start("TokenRenderer-init"); + renderer = zoneRenderer; + zone = renderer.getZone(); + grid = zone.getGrid(); + isSquare = GridFactory.getGridType(grid).equals(GridFactory.SQUARE); + scale = renderer.getScale(); + initialised = true; + timer.stop("TokenRenderer-init"); + } + + public void renderToken(Token token, Map renderInfo) { + timer.start("TokenRenderer-renderToken"); + this.token = token; + if (haveSufficientRenderInfo(renderInfo)) { + compareStates(); + paintTokenImage(); + } else { + log.debug("Not enough render info in " + renderInfo); + } + timer.stop("TokenRenderer-renderToken"); + } + + private boolean haveSufficientRenderInfo(Map renderInfo) { + try { + // has minimum keys required + if (!(renderInfo.containsKey(GRAPHICS) && renderInfo.containsKey(LOCATION))) { + return false; + } + g2d = (Graphics2D) renderInfo.get(GRAPHICS); + location = (TokenLocation) renderInfo.get(LOCATION); + if (renderInfo.containsKey(OPACITY)) { + opacity = (float) renderInfo.get(OPACITY); + } else { + opacity = 1f; + } + if (renderInfo.containsKey(CLIP)) { + clip = (Area) renderInfo.get(CLIP); + } else { + clip = null; + } + } catch (ClassCastException cce) { + log.debug(cce.getLocalizedMessage(), cce); + return false; + } + return true; + } + + private void compareStates() { + TokenState currentState = createRecord(token); + isoFigure = + grid.isIsometric() + && !currentState.flippedIso + && currentState.shape.equals(Token.TokenShape.FIGURE); + canSpin = currentState.shape.equals(Token.TokenShape.TOP_DOWN); + boolean updateStoredImage = false; + if (!tokenStateMap.containsKey(token)) { + updateStoredImage = true; + } else { + updateStoredImage = !tokenStateMap.get(token).equals(currentState); + } + tokenStateMap.put(token, currentState); + if (updateStoredImage || !renderImageMap.containsKey(token)) { + renderImage = ImageUtil.getTokenRenderImage(token, renderer); + renderImageMap.put(token, renderImage); + } else { + renderImage = renderImageMap.get(token); + } + } + + private void paintTokenImage() { + timer.start("TokenRenderer-paintTokenImage"); + Shape oldClip = g2d.getClip(); + AffineTransform oldAT = g2d.getTransform(); + Composite oldComposite = g2d.getComposite(); + // centre image + double imageCx = -renderImage.getWidth() / 2d; + double imageCy = -renderImage.getHeight() / (isoFigure ? 4d / 3d : 2d); + AffineTransform imageTransform = + AffineTransform.getTranslateInstance( + imageCx + token.getAnchorX() * scale, imageCy + token.getAnchorY() * scale); + + if (clip != null) { + g2d.setClip(clip); + } + g2d.translate(location.x + location.scaledWidth / 2d, location.y + location.scaledHeight / 2d); + + if (token.hasFacing() && canSpin) { + g2d.rotate(Math.toRadians(token.getFacingInDegrees())); + } + if (opacity < 1.0f) { + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)); + } + + g2d.drawImage(renderImage, imageTransform, renderer); + g2d.setStroke(new BasicStroke(1f)); + + g2d.setComposite(oldComposite); + g2d.setTransform(oldAT); + g2d.setClip(oldClip); + timer.stop("TokenRenderer-paintTokenImage"); + } + + private TokenState createRecord(Token token) { + return new TokenState( + token.getFacing(), + token.getImageRotation(), + token.getScaleX(), + token.getScaleY(), + token.getSizeScale(), + token.isFlippedIso(), + token.isFlippedX(), + token.isFlippedY(), + token.getShape(), + token.isSnapToScale(), + token.getFootprint(grid)); + } + + public void zoomChanged() { + if (initialised) { + renderImageMap.clear(); + scale = renderer.getScale(); + facingArrowRenderer.setScale(scale); + } + } + + @Subscribe + private void onGridChanged(GridChanged event) { + if (this.initialised) { + setRenderer(this.renderer); + facingArrowRenderer.setRenderer(this.renderer); + } + } + + public void paintFacingArrow( + Graphics2D tokenG, Token token, Rectangle footprintBounds, TokenLocation location) { + if (!facingArrowRenderer.isInitialised()) { + facingArrowRenderer.setRenderer(renderer); + } + facingArrowRenderer.paintArrow(tokenG, token, footprintBounds, location, renderer); + } + + private record TokenState( + int facing, + double imageRotation, + double scaleX, + double scaleY, + double sizeScale, + boolean flippedIso, + boolean flippedX, + boolean flippedY, + Token.TokenShape shape, + boolean snapToScale, + TokenFootprint footprint) {} +} From 401ddc02dbf05fa5ba0d533055fb619846a38c0c Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 23:00:01 +0800 Subject: [PATCH 18/24] Facing-arrow painting moved to FacingArrowRenderer --- .../tokenRender/FacingArrowRenderer.java | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java 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 new file mode 100644 index 0000000000..4221ab0da9 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java @@ -0,0 +1,235 @@ +/* + * 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.AffineTransform; +import java.awt.geom.Path2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import net.rptools.lib.CodeTimer; +import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.ui.zone.renderer.TokenLocation; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.events.MapToolEventBus; +import net.rptools.maptool.model.Grid; +import net.rptools.maptool.model.Token; +import net.rptools.maptool.model.Zone; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +public class FacingArrowRenderer { + private static final Logger log = LogManager.getLogger(FacingArrowRenderer.class); + private final CodeTimer timer; + private final Map> quivers = new HashMap<>(); + Rectangle footprintBounds; + Token.TokenShape tokenType; + ArrowType arrowType; + double scale; + private boolean initialised = false; + private ZoneRenderer renderer; + private boolean isIsometric; + private ArrayList fillColours = new ArrayList<>(); + private Color fillColour = Color.YELLOW; + private Color borderColour = Color.DARK_GRAY; + + FacingArrowRenderer() { + new MapToolEventBus().getMainEventBus().register(this); + for (int i = 0; i <= 90; i++) { + fillColours.add(new Color(1 - 0.5f / 90f * i, 1 - 0.5f / 90f * i, 0)); + } + for (int i = 89; i >= 0; i--) { + fillColours.add(fillColours.get(i)); + } + System.out.println(fillColours); + timer = CodeTimer.get(); + } + + public boolean isInitialised() { + return initialised; + } + + double getScale() { + return scale; + } + + public void setScale(double scale) { + this.scale = scale; + } + + public void paintArrow( + Graphics2D tokenG, + Token token, + Rectangle footprintBounds_, + TokenLocation location, + ZoneRenderer zoneRenderer) { + if (!renderer.equals(zoneRenderer)) { + setRenderer(zoneRenderer); + } + tokenType = token.getShape(); + if (tokenType.equals(Token.TokenShape.TOP_DOWN) && !AppPreferences.forceFacingArrow.get()) { + return; + } + if (tokenType.equals(Token.TokenShape.FIGURE) + && token.getHasImageTable() + && !AppPreferences.forceFacingArrow.get()) { + return; + } + + timer.start("ArrowRenderer-paintArrow"); + AffineTransform oldAT = tokenG.getTransform(); + Grid grid = renderer.getZone().getGrid(); + footprintBounds = footprintBounds_; + double facing = token.getFacing(); + facing = isIsometric ? facing + 45 : facing; + while (facing < 0) { + facing += 360; + } + if (facing > 360) { + facing %= 360; + } + double radFacing = Math.toRadians(facing); + double cx = location.x + location.scaledWidth / 2d; + double cy = location.y + location.scaledHeight / 2d; + + Shape facingArrow = getArrow(token); + + facingArrow = AffineTransform.getRotateInstance(-radFacing).createTransformedShape(facingArrow); + facingArrow = + AffineTransform.getScaleInstance(getScale(), isIsometric ? getScale() / 2d : getScale()) + .createTransformedShape(facingArrow); + + if (tokenType.equals(Token.TokenShape.SQUARE) && !isIsometric) { + double xp = location.scaledWidth / 2; + double yp = location.scaledHeight / 2; + if (facing >= 45 && facing <= 135 || facing >= 225 && facing <= 315) { + xp = yp / Math.tan(Math.toRadians(facing)); + if (facing > 180) { + xp = -xp; + yp = -yp; + } + } else { + yp = xp * Math.tan(Math.toRadians(facing)); + if (facing > 90 && facing < 270) { + xp = -xp; + yp = -yp; + } + } + cx += xp; + cy -= yp; + } + tokenG.translate(cx, cy); + + if (tokenType.equals(Token.TokenShape.FIGURE) && facing <= 180) { + tokenG.setColor(fillColours.get((int) facing)); + } else { + tokenG.setColor(fillColour); + } + + tokenG.fill(facingArrow); + tokenG.setColor(borderColour); + tokenG.draw(facingArrow); + + tokenG.setTransform(oldAT); + timer.stop("ArrowRenderer-paintArrow"); + } + + public void setRenderer(ZoneRenderer zoneRenderer) { + timer.start("ArrowRenderer-init"); + renderer = zoneRenderer; + Zone zone = renderer.getZone(); + isIsometric = zone.getGrid().isIsometric(); + scale = renderer.getScale(); + initialised = true; + timer.stop("ArrowRenderer-init"); + } + + private Shape getArrow(Token token) { + if ((!AppPreferences.forceFacingArrow.get() && tokenType.equals(Token.TokenShape.TOP_DOWN)) + || (!AppPreferences.forceFacingArrow.get() && token.getHasImageTable())) { + return null; + } + Shape arrow = new Path2D.Double(); + if (isIsometric) { + arrowType = ArrowType.ISOMETRIC; + } else { + switch (tokenType) { + case FIGURE, CIRCLE -> arrowType = ArrowType.CIRCLE; + case SQUARE -> arrowType = ArrowType.SQUARE; + default -> arrowType = ArrowType.NONE; + } + } + double size = footprintBounds.getWidth() / 2d; + Map quiver; + if (quivers.containsKey(arrowType)) { + quiver = quivers.get(arrowType); + } else { + quiver = new HashMap<>(); + } + if (quiver.containsKey(size)) { + return quiver.get(size); + } else { + switch (arrowType) { + case CIRCLE -> arrow = getCircleFacingArrow(size); + case ISOMETRIC -> arrow = getFigureFacingArrow(size); + case SQUARE -> arrow = getSquareFacingArrow(size); + } + quiver.put(size, arrow); + quivers.put(arrowType, quiver); + } + return arrow; + } + + protected Shape getCircleFacingArrow(double size) { + double base = size * .75; + double y = size * .35; + Path2D facingArrow = new Path2D.Double(); + facingArrow.moveTo(base, -y); + facingArrow.lineTo(size, 0); + facingArrow.lineTo(base, y); + facingArrow.lineTo(base, -y); + return facingArrow; + } + + protected Shape getFigureFacingArrow(double size) { + double base = size * .75; + double y = size * .35; + Path2D facingArrow = new Path2D.Double(); + facingArrow.moveTo(base, -y); + facingArrow.lineTo(size, 0); + facingArrow.lineTo(base, y); + facingArrow.lineTo(base, -y); + return facingArrow; + } + + protected Shape getSquareFacingArrow(double size) { + double base = size * .75; + double y = size * .35; + Path2D facingArrow = new Path2D.Double(); + facingArrow.moveTo(0, 0); + facingArrow.lineTo(-(size - base), -y); + facingArrow.lineTo(-(size - base), y); + facingArrow.lineTo(0, 0); + return facingArrow; + } + + private enum ArrowType { + CIRCLE, + ISOMETRIC, + SQUARE, + NONE + } +} From 2017845c174a8f855b07c3119473a98137a80a02 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Fri, 13 Dec 2024 23:07:51 +0800 Subject: [PATCH 19/24] Code cleanup Remove old layout panel --- .../token/dialog/edit/TokenLayoutPanel.java | 230 ------------------ .../dialog/edit/TokenLayoutPanelHelper.java | 18 +- .../dialog/edit/TokenLayoutRenderPanel.java | 11 +- 3 files changed, 11 insertions(+), 248 deletions(-) delete mode 100644 src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java deleted file mode 100644 index c721955573..0000000000 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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.token.dialog.edit; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.TexturePaint; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseMotionAdapter; -import java.awt.geom.Area; -import java.awt.image.BufferedImage; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import net.rptools.lib.MD5Key; -import net.rptools.maptool.client.AppStyle; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.client.swing.SwingUtil; -import net.rptools.maptool.client.ui.theme.Images; -import net.rptools.maptool.client.ui.theme.RessourceManager; -import net.rptools.maptool.language.I18N; -import net.rptools.maptool.model.Token; -import net.rptools.maptool.model.Token.TokenShape; -import net.rptools.maptool.model.Zone; -import net.rptools.maptool.util.ImageManager; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Support class used by the token editor dialog on the "Properties" tab to allow a token's image to - * be moved around within a one-cell grid area. Scaling is supported using the mousewheel and - * position is supported using left-drag. We should add rotation ability using Shift-mousewheel as - * well. - * - * @author trevor - */ -public class TokenLayoutPanel extends JPanel { - private static final Logger log = LogManager.getLogger(TokenLayoutPanel.class); - private Token token; - private int dragOffsetX; - private int dragOffsetY; - private MD5Key tokenImage; - - public TokenLayoutPanel() { - addMouseWheelListener( - e -> { - int wheelMovement = e.getWheelRotation(); - // Not for snap-to-scale - if (!token.isSnapToScale() || wheelMovement == 0) { - return; - } - double delta = wheelMovement > 0 ? -.1 : .1; - if (SwingUtil.isShiftDown(e)) { - // Nothing yet, as changing the facing isn't the right way to handle it -- - // the image itself really should be rotated. And it's probably better to - // not simply store a Transform but to create a new image. We could - // store an AffineTransform until the dialog is closed and then create - // the new image. But the amount of rotation needs to be saved so - // that future adjustments can return back to the original image (as - // a way of reducing round off error from multiple rotations). - } - double scale = token.getSizeScale() + delta; - log.debug(() -> "wheel=" + wheelMovement + ", delta=" + delta); - - // Range - scale = Math.max(.1, scale); - scale = Math.min(3, scale); - token.setSizeScale(scale); - repaint(); - }); - addMouseListener( - new MouseAdapter() { - String old; - - @Override - public void mousePressed(MouseEvent e) { - dragOffsetX = e.getX(); - dragOffsetY = e.getY(); - } - - @Override - public void mouseEntered(MouseEvent e) { - old = MapTool.getFrame().getStatusMessage(); - MapTool.getFrame() - .setStatusMessage(I18N.getString("EditTokenDialog.status.layout.instructions")); - } - - @Override - public void mouseExited(MouseEvent e) { - if (old != null) MapTool.getFrame().setStatusMessage(old); - } - - @Override - public void mouseClicked(MouseEvent e) { - if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { - dragOffsetX = 0; - dragOffsetY = 0; - token.setAnchor(0, 0); - token.setSizeScale(1.0); - repaint(); - } - } - }); - addMouseMotionListener( - new MouseMotionAdapter() { - @Override - public void mouseDragged(MouseEvent e) { - int dx = e.getX() - dragOffsetX; - int dy = e.getY() - dragOffsetY; - - Zone zone = MapTool.getFrame().getCurrentZoneRenderer().getZone(); - - int gridSize = zone.getGrid().getSize(); - int halfGridSize = gridSize / 2; - int maxXoff = Math.max(halfGridSize, token.getBounds(zone).width - gridSize); - int maxYoff = Math.max(halfGridSize, token.getBounds(zone).height - gridSize); - - int offX = Math.min(maxXoff, Math.max(token.getAnchor().x + dx, -maxXoff)); - int offY = Math.min(maxYoff, Math.max(token.getAnchor().y + dy, -maxYoff)); - - token.setAnchor(offX, offY); - dragOffsetX = e.getX(); - dragOffsetY = e.getY(); - repaint(); - } - }); - } - - public double getSizeScale() { - return token.getSizeScale(); - } - - public int getAnchorX() { - return token.getAnchor().x; - } - - public int getAnchorY() { - return token.getAnchor().y; - } - - public void setToken(Token token) { - this.token = new Token(token); - setTokenImage(token.getImageAssetId()); - } - - public MD5Key getTokenImage() { - return tokenImage; - } - - public void setTokenImage(MD5Key tokenImage) { - this.tokenImage = tokenImage; - } - - @Override - protected void paintComponent(Graphics g) { - Dimension size = getSize(); - Zone zone = MapTool.getFrame().getCurrentZoneRenderer().getZone(); - - // Gather info - BufferedImage image = ImageManager.getImage(getTokenImage()); - - Rectangle tokenSize = token.getBounds(zone); - Dimension imgSize = new Dimension(image.getWidth(), image.getHeight()); - - // If figure we need to calculate an additional offset for the token height - double iso_ho = 0; - if (token.getShape() == TokenShape.FIGURE) { - double th = token.getHeight() * (double) tokenSize.width / token.getWidth(); - iso_ho = tokenSize.height - th; - tokenSize = new Rectangle(tokenSize.x, tokenSize.y - (int) iso_ho, tokenSize.width, (int) th); - } - - SwingUtil.constrainTo(imgSize, tokenSize.width, tokenSize.height); - - Point centerPoint = new Point(size.width / 2, size.height / 2); - Graphics2D g2d = (Graphics2D) g; - - var panelTexture = RessourceManager.getImage(Images.TEXTURE_PANEL); - // Background - ((Graphics2D) g) - .setPaint( - new TexturePaint( - panelTexture, - new Rectangle(0, 0, panelTexture.getWidth(), panelTexture.getHeight()))); - g2d.fillRect(0, 0, size.width, size.height); - AppStyle.shadowBorder.paintWithin((Graphics2D) g, 0, 0, size.width, size.height); - - // Grid - if (zone.getGrid().getCapabilities().isSnapToGridSupported()) { - Area gridShape = zone.getGrid().getCellShape(); - int offsetX = (size.width - gridShape.getBounds().width) / 2; - int offsetY = (size.height - gridShape.getBounds().height) / 2; - g2d.setColor(Color.black); - - // Add horizontal and vertical lines to help with centering - g2d.drawLine( - 0, (size.height - (int) iso_ho) / 2, size.width, (size.height - (int) iso_ho) / 2); - g2d.drawLine(size.width / 2, 0, size.width / 2, (size.height - (int) iso_ho)); - - offsetY = offsetY - (int) (iso_ho / 2); - g2d.translate(offsetX, offsetY); - g2d.draw(gridShape); - g2d.translate(-offsetX, -offsetY); - } - // Token - g2d.drawImage( - image, - centerPoint.x - imgSize.width / 2 + token.getAnchor().x, - centerPoint.y - imgSize.height / 2 + token.getAnchor().y, - imgSize.width, - imgSize.height, - this); - } -} diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java index 64f8a057e1..eb95da6400 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java @@ -813,19 +813,7 @@ void init() { (int) footprintBounds.getHeight(), BufferedImage.TYPE_4BYTE_ABGR_PRE); } - - if (isIsoFigure) { - /* If figure we calculate an additional offset for token height outside footprint */ - double imageFitRatio = - footprintBounds.getWidth() / tokenImage.getWidth(); // scale for width - if (tokenImage.getHeight() * footprintBounds.getWidth() / tokenImage.getWidth() - > 2 * footprintBounds.getHeight()) { - /* if this results in being more than twice the footprint height, use height instead */ - imageFitRatio = footprintBounds.getHeight() * 2 / tokenImage.getHeight(); - } - double th = tokenImage.getHeight() * imageFitRatio; - iso_figure_ho = (footprintBounds.getHeight() - th); - } + iso_figure_ho = ImageUtil.getIsoFigureHeightOffset(mirrorToken, footprintBounds); workImage = ImageUtil.getScaledTokenImage(tokenImage, mirrorToken, grid, zoomFactor); workImage = getFlippedImage(workImage); @@ -1143,7 +1131,8 @@ void paintFootprint(Graphics g, double zoomFactor) { g2d.setColor(colours[4]); double footprintScale = footprint.getScale(); - double yCorrection = isIsoFigure ? footprintScale * footprintBounds.getHeight() / 2d : 0; + double yCorrection = + isIsoFigure ? footprintScale * footprintBounds.getHeight() / 2d * zoomFactor : 0; Shape scaledOutline, scaledFill; /* for drawing sub-cell-sizes */ @@ -1227,7 +1216,6 @@ void paintToken(Graphics g, boolean translucent) { g2d.translate(centrePoint.getX(), centrePoint.getY()); g2d.translate(getTokenAnchorX() * zoomFactor, getTokenAnchorY() * zoomFactor); - // g2d.scale(zoomFactor, zoomFactor); Composite oldAc = g2d.getComposite(); diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java index e7c2a6e880..5715776b3d 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java @@ -150,8 +150,13 @@ public void reset(Token token) { /** Work out a zoom factor to fit the token on screen with a half cell border */ protected void calcZoomFactor() { - Rectangle2D fpBounds = helper.footprintBounds; - if (token == null || getSize().height == 0 || size == null || fpBounds == null) { + Rectangle2D fpBounds = + new Rectangle2D.Double( + helper.footprintBounds.getX(), + helper.footprintBounds.getY(), + helper.footprintBounds.getWidth(), + helper.footprintBounds.getHeight()); + if (token == null || getSize().height == 0 || size == null) { return; } fpBounds.setRect( @@ -178,7 +183,7 @@ protected void calcZoomFactor() { double fitHeight = fpBounds.getHeight() + helper.grid.getCellHeight() / 2; // which axis has the least space to grow boolean scaleToWidth = size.getWidth() - fitWidth < size.getHeight() - fitHeight; - // set the zoomfactor + // set the zoom-factor double newZoom = scaleToWidth ? size.getWidth() / fitWidth : size.getHeight() / fitHeight; setZoomFactor(newZoom); From 2b198d67790fe6efb41580f392a269896cbb4a80 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:22:26 +0800 Subject: [PATCH 20/24] Code cleanup Icons embedded --- .../dialog/edit/TokenLayoutPanelHelper.java | 42 ++++--------------- .../tokenRender/FacingArrowRenderer.java | 1 - .../rptools/maptool/client/image/scale.svg | 9 ++++ .../rptools/maptool/client/image/scaleHor.svg | 7 ++++ .../maptool/client/image/scaleVert.svg | 7 ++++ 5 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 src/main/resources/net/rptools/maptool/client/image/scale.svg create mode 100644 src/main/resources/net/rptools/maptool/client/image/scaleHor.svg create mode 100644 src/main/resources/net/rptools/maptool/client/image/scaleVert.svg diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java index eb95da6400..39fc233c79 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java @@ -14,7 +14,6 @@ */ package net.rptools.maptool.client.ui.token.dialog.edit; -import com.formdev.flatlaf.FlatIconColors; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.FlatSVGIcon; import com.formdev.flatlaf.extras.components.FlatButton; @@ -24,10 +23,6 @@ import java.awt.image.BufferedImage; import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.util.*; import java.util.function.Function; @@ -463,7 +458,7 @@ private void setFootprint(TokenFootprint fp) { } double xFix = -aggregateBounds.getCenterX(); double yFix = -aggregateBounds.getCenterY(); - cellCentres.replaceAll(pt -> pt = new Point2D.Double(pt.getX() + xFix, pt.getY() + yFix)); + cellCentres.replaceAll(pt -> new Point2D.Double(pt.getX() + xFix, pt.getY() + yFix)); } private void setCentredFootprintBounds() { @@ -578,8 +573,8 @@ public void initButtons() { getScaleButton().setIcon(scaleIcons[scaleAxis]); getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " "); - AbstractButton layoutHelpButton = new FlatButton(); - ((FlatButton) layoutHelpButton).setButtonType(FlatButton.ButtonType.help); + FlatButton layoutHelpButton = new FlatButton(); + layoutHelpButton.setButtonType(FlatButton.ButtonType.help); parent.replaceComponent("layoutTabPanel", "layoutHelpButton", layoutHelpButton); layoutHelpButton.setToolTipText(helpText); layoutHelpButton.addActionListener(e -> showHelp()); @@ -717,35 +712,12 @@ private void setControlValues() { getZoomSpinner().setValue(1d); } - String scaleStartSVG = - ""; - String scaleNSSVG = - ""; - String scaleEWSVG = - ""; - String scaleEndSVG = ""; - - public static ImageIcon makeSVGIcon(int size, String svg) { - InputStream stream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); - FlatSVGIcon.ColorFilter colorFilter = new FlatSVGIcon.ColorFilter(); - /* Convert a the original color into another one supposedly */ - colorFilter.add(Color.BLACK, UI_DEFAULTS.getColor(FlatIconColors.OBJECTS_BLACK_TEXT)); - colorFilter.add(Color.WHITE, UI_DEFAULTS.getColor(FlatIconColors.ACTIONS_BLUE_DARK)); - try { - FlatSVGIcon flatSVGIcon = new FlatSVGIcon(stream); - flatSVGIcon.setColorFilter(colorFilter); - return flatSVGIcon.derive(size, size); - } catch (IOException e) { - return null; - } - } - ImageIcon[] scaleIcons = new ImageIcon[3]; - private void createButtonIcons() { - scaleIcons[0] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleNSSVG + scaleEWSVG + scaleEndSVG); - scaleIcons[1] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleEWSVG + scaleEndSVG); - scaleIcons[2] = makeSVGIcon(ICON_SIZE, scaleStartSVG + scaleNSSVG + scaleEndSVG); + String iconBase = "net/rptools/maptool/client/image/"; + scaleIcons[0] = new FlatSVGIcon(iconBase + "scale.svg", ICON_SIZE, ICON_SIZE); + scaleIcons[1] = new FlatSVGIcon(iconBase + "scaleHor.svg", ICON_SIZE, ICON_SIZE); + scaleIcons[2] = new FlatSVGIcon(iconBase + "scaleVert.svg", ICON_SIZE, ICON_SIZE); } private class ScaleButtonListener implements ActionListener { 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 4221ab0da9..e4ab988ac7 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 @@ -54,7 +54,6 @@ public class FacingArrowRenderer { for (int i = 89; i >= 0; i--) { fillColours.add(fillColours.get(i)); } - System.out.println(fillColours); timer = CodeTimer.get(); } diff --git a/src/main/resources/net/rptools/maptool/client/image/scale.svg b/src/main/resources/net/rptools/maptool/client/image/scale.svg new file mode 100644 index 0000000000..32c8b5fa42 --- /dev/null +++ b/src/main/resources/net/rptools/maptool/client/image/scale.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg b/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg new file mode 100644 index 0000000000..c722aeb276 --- /dev/null +++ b/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg b/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg new file mode 100644 index 0000000000..ff31922842 --- /dev/null +++ b/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg @@ -0,0 +1,7 @@ + + + + + + + From 36146cd9b43ec322c2b31780f9ef04e5e373aa9e Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:26:15 +0800 Subject: [PATCH 21/24] Spotless exception removed --- .../dialog/edit/TokenLayoutPanelHelper.java | 237 ++++++++++++------ 1 file changed, 167 insertions(+), 70 deletions(-) diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java index 39fc233c79..7ca5faa91e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java +++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java @@ -147,79 +147,175 @@ public void setParentRoot(JRootPane rp) { private final String helpText = assembleHelpText(); - // @formatter:off - // spotless:off - /* Component Getters */ - public JComboBox getSizeCombo(){ if (sizeCombo == null) sizeCombo = (JComboBox) parent.getComponent("size"); return sizeCombo; } - /* Labels */ - public JLabel getScaleLabel(){ if (scaleLabel == null) scaleLabel = parent.getLabel("scaleLabel"); return scaleLabel; } - /* Spinners */ - public JSpinner getAnchorXSpinner() { if (anchorXSpinner == null) anchorXSpinner = parent.getSpinner("anchorXSpinner"); return anchorXSpinner; } - public JSpinner getAnchorYSpinner() { if (anchorYSpinner == null) anchorYSpinner = parent.getSpinner("anchorYSpinner"); return anchorYSpinner; } - public JSpinner getRotationSpinner() { if (rotationSpinner == null) rotationSpinner = parent.getSpinner("rotationSpinner"); return rotationSpinner; } - public JSpinner getScaleSpinner() { if (scaleSpinner == null) scaleSpinner = parent.getSpinner("scaleSpinner"); return scaleSpinner; } - public JSpinner getZoomSpinner() { if (zoomSpinner == null) zoomSpinner = parent.getSpinner("zoomSpinner"); return zoomSpinner; } - /* Sliders */ - public JSlider getAnchorXSlider() { if (anchorXSlider == null) anchorXSlider = (JSlider) parent.getComponent("anchorXSlider"); return anchorXSlider; } - public JSlider getAnchorYSlider() { if (anchorYSlider == null) anchorYSlider = (JSlider) parent.getComponent("anchorYSlider"); return anchorYSlider; } - public JSlider getRotationSlider() { if (rotationSlider == null) rotationSlider = (JSlider) parent.getComponent("rotationSlider"); return rotationSlider; } - public JSlider getScaleSlider() { if (scaleSlider == null) scaleSlider = (JSlider) parent.getComponent("scaleSlider"); return scaleSlider; } - public JSlider getZoomSlider() { if (zoomSlider == null) zoomSlider = (JSlider) parent.getComponent("zoomSlider"); return zoomSlider; } - /* Buttons */ - public AbstractButton getScaleButton() { if (scaleButton == null) scaleButton = parent.getButton("scaleButton"); return scaleButton; } - /* Panel */ - public TokenLayoutRenderPanel getRenderPanel() { return renderPanel; } - /* Linked controls */ - SpinnerSliderPaired anchorXPair, anchorYPair, rotationPair, scalePair, zoomPair; - - - /* Token value getters pointing to the mirror token */ - public double getTokenSizeScale() { return mirrorToken.getSizeScale(); } - public double getTokenScaleX() { return mirrorToken.getScaleX(); } - public double getTokenScaleY() { return mirrorToken.getScaleY(); } - public double getTokenImageRotation() { return mirrorToken.getImageRotation(); } - public int getTokenAnchorX() { return mirrorToken.getAnchorX(); } - public int getTokenAnchorY() { return mirrorToken.getAnchorY(); } - public boolean getTokenFlippedX() { return mirrorToken.isFlippedX(); } - public boolean getTokenFlippedY() { return mirrorToken.isFlippedY(); } - public boolean getTokenFlippedIso() { return mirrorToken.isFlippedIso(); } - - /* Token value setters pointing to the mirror token */ - protected void setTokenScaleX(double scale) { mirrorToken.setScaleX(scale);} - protected void setTokenScaleY(double scale) { mirrorToken.setScaleY(scale); } - protected void setTokenSizeScale(double scale) { mirrorToken.setSizeScale(scale); } - protected void setTokenAnchorX(Number x) { mirrorToken.setAnchorX(x.intValue()); } - protected void setTokenAnchorY(Number y) { mirrorToken.setAnchorY(y.intValue()); } - protected void setTokenImageRotation(Number value) { mirrorToken.setImageRotation(MathUtil.doublePrecision(value.doubleValue(), 4)); } - protected void setTokenFlipIso(Boolean b) { - mirrorToken.setFlippedIso(b); - if(flipStates.contains(FlipState.ISOMETRIC) && !b){ - flipStates.remove(FlipState.ISOMETRIC); - } else if(!flipStates.contains(FlipState.ISOMETRIC) && b){ - flipStates.add(FlipState.ISOMETRIC); - } - iFeelDirty(); + /* Component Getters */ + public JComboBox getSizeCombo() { + if (sizeCombo == null) sizeCombo = (JComboBox) parent.getComponent("size"); + return sizeCombo; + } + + /* Labels */ + public JLabel getScaleLabel() { + if (scaleLabel == null) scaleLabel = parent.getLabel("scaleLabel"); + return scaleLabel; + } + + /* Spinners */ + public JSpinner getAnchorXSpinner() { + if (anchorXSpinner == null) anchorXSpinner = parent.getSpinner("anchorXSpinner"); + return anchorXSpinner; + } + + public JSpinner getAnchorYSpinner() { + if (anchorYSpinner == null) anchorYSpinner = parent.getSpinner("anchorYSpinner"); + return anchorYSpinner; + } + + public JSpinner getRotationSpinner() { + if (rotationSpinner == null) rotationSpinner = parent.getSpinner("rotationSpinner"); + return rotationSpinner; + } + + public JSpinner getScaleSpinner() { + if (scaleSpinner == null) scaleSpinner = parent.getSpinner("scaleSpinner"); + return scaleSpinner; + } + + public JSpinner getZoomSpinner() { + if (zoomSpinner == null) zoomSpinner = parent.getSpinner("zoomSpinner"); + return zoomSpinner; + } + + /* Sliders */ + public JSlider getAnchorXSlider() { + if (anchorXSlider == null) anchorXSlider = (JSlider) parent.getComponent("anchorXSlider"); + return anchorXSlider; + } + + public JSlider getAnchorYSlider() { + if (anchorYSlider == null) anchorYSlider = (JSlider) parent.getComponent("anchorYSlider"); + return anchorYSlider; + } + + public JSlider getRotationSlider() { + if (rotationSlider == null) rotationSlider = (JSlider) parent.getComponent("rotationSlider"); + return rotationSlider; + } + + public JSlider getScaleSlider() { + if (scaleSlider == null) scaleSlider = (JSlider) parent.getComponent("scaleSlider"); + return scaleSlider; + } + + public JSlider getZoomSlider() { + if (zoomSlider == null) zoomSlider = (JSlider) parent.getComponent("zoomSlider"); + return zoomSlider; + } + + /* Buttons */ + public AbstractButton getScaleButton() { + if (scaleButton == null) scaleButton = parent.getButton("scaleButton"); + return scaleButton; + } + + /* Panel */ + public TokenLayoutRenderPanel getRenderPanel() { + return renderPanel; + } + + /* Linked controls */ + SpinnerSliderPaired anchorXPair, anchorYPair, rotationPair, scalePair, zoomPair; + + /* Token value getters pointing to the mirror token */ + public double getTokenSizeScale() { + return mirrorToken.getSizeScale(); + } + + public double getTokenScaleX() { + return mirrorToken.getScaleX(); + } + + public double getTokenScaleY() { + return mirrorToken.getScaleY(); + } + + public double getTokenImageRotation() { + return mirrorToken.getImageRotation(); + } + + public int getTokenAnchorX() { + return mirrorToken.getAnchorX(); + } + + public int getTokenAnchorY() { + return mirrorToken.getAnchorY(); + } + + public boolean getTokenFlippedX() { + return mirrorToken.isFlippedX(); + } + + public boolean getTokenFlippedY() { + return mirrorToken.isFlippedY(); + } + + public boolean getTokenFlippedIso() { + return mirrorToken.isFlippedIso(); + } + + /* Token value setters pointing to the mirror token */ + protected void setTokenScaleX(double scale) { + mirrorToken.setScaleX(scale); + } + + protected void setTokenScaleY(double scale) { + mirrorToken.setScaleY(scale); + } + + protected void setTokenSizeScale(double scale) { + mirrorToken.setSizeScale(scale); + } + + protected void setTokenAnchorX(Number x) { + mirrorToken.setAnchorX(x.intValue()); + } + + protected void setTokenAnchorY(Number y) { + mirrorToken.setAnchorY(y.intValue()); + } + + protected void setTokenImageRotation(Number value) { + mirrorToken.setImageRotation(MathUtil.doublePrecision(value.doubleValue(), 4)); + } + + protected void setTokenFlipIso(Boolean b) { + mirrorToken.setFlippedIso(b); + if (flipStates.contains(FlipState.ISOMETRIC) && !b) { + flipStates.remove(FlipState.ISOMETRIC); + } else if (!flipStates.contains(FlipState.ISOMETRIC) && b) { + flipStates.add(FlipState.ISOMETRIC); } - protected void setTokenFlipX(Boolean b) { - mirrorToken.setFlippedX(b); - if(flipStates.contains(FlipState.HORIZONTAL) && !b){ - flipStates.remove(FlipState.HORIZONTAL); - } else if(!flipStates.contains(FlipState.HORIZONTAL) && b){ - flipStates.add(FlipState.HORIZONTAL); - } - iFeelDirty(); + iFeelDirty(); + } + + protected void setTokenFlipX(Boolean b) { + mirrorToken.setFlippedX(b); + if (flipStates.contains(FlipState.HORIZONTAL) && !b) { + flipStates.remove(FlipState.HORIZONTAL); + } else if (!flipStates.contains(FlipState.HORIZONTAL) && b) { + flipStates.add(FlipState.HORIZONTAL); } - protected void setTokenFlipY(Boolean b) { - mirrorToken.setFlippedY(b); - if(flipStates.contains(FlipState.VERTICAL) && !b){ - flipStates.remove(FlipState.VERTICAL); - } else if(!flipStates.contains(FlipState.VERTICAL) && b){ - flipStates.add(FlipState.VERTICAL); - } - iFeelDirty(); + iFeelDirty(); + } + + protected void setTokenFlipY(Boolean b) { + mirrorToken.setFlippedY(b); + if (flipStates.contains(FlipState.VERTICAL) && !b) { + flipStates.remove(FlipState.VERTICAL); + } else if (!flipStates.contains(FlipState.VERTICAL) && b) { + flipStates.add(FlipState.VERTICAL); } - //@formatter:on - //spotless:on + iFeelDirty(); + } /** * There is one spinner/slider for three different scale settings. This sets the value for the @@ -713,6 +809,7 @@ private void setControlValues() { } ImageIcon[] scaleIcons = new ImageIcon[3]; + private void createButtonIcons() { String iconBase = "net/rptools/maptool/client/image/"; scaleIcons[0] = new FlatSVGIcon(iconBase + "scale.svg", ICON_SIZE, ICON_SIZE); From cd56de7618e4dc7c6844e4f4e0213d2199584d98 Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:47:20 +0800 Subject: [PATCH 22/24] Fixed getScaledTokenImage returning incorrect scaling for free-size images --- .../java/net/rptools/lib/image/ImageUtil.java | 404 +++++++++--------- 1 file changed, 207 insertions(+), 197 deletions(-) diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index f8e08a2015..0e8164a23c 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -87,14 +87,6 @@ public static BufferedImage replaceColor(BufferedImage src, int sourceRGB, int r return src; } - public static int negativeColourInt(int rgb) { - int r = 255 - ((rgb >> 16) & 0xFF); - int g = 255 - ((rgb >> 8) & 0xFF); - int b = 255 - (rgb & 0xFF); - int negativeRGB = (r << 16) | (g << 8) | b; - return negativeRGB; - } - public static BufferedImage negativeImage(BufferedImage originalImage) { // Get the dimensions of the image int width = originalImage.getWidth(); @@ -111,6 +103,14 @@ public static BufferedImage negativeImage(BufferedImage originalImage) { return negativeImage; } + public static int negativeColourInt(int rgb) { + int r = 255 - ((rgb >> 16) & 0xFF); + int g = 255 - ((rgb >> 8) & 0xFF); + int b = 255 - (rgb & 0xFF); + int negativeRGB = (r << 16) | (g << 8) | b; + return negativeRGB; + } + /** * Scales the provided image with the ZoneRenderer scale, the token footprint, and the token's * layout scale factors. @@ -126,49 +126,59 @@ public static BufferedImage getScaledTokenImage(BufferedImage bi, Token token, Z public static BufferedImage getScaledTokenImage( BufferedImage img, Token token, Grid grid, double zoom) { - TokenFootprint footprint = token.getFootprint(grid); - Rectangle2D footprintBounds = footprint.getBounds(grid); // , new CellPoint(0, 0)); - // except gridless, this should be 1 for footprints larger than the grid - double fpS = footprint.getScale(); - // size - double fpW, fpH; - // multiply by zoom level to prevent multiple scaling ops which lose definition, i.e. scale once - if (GridFactory.getGridType(grid).equals(GridFactory.NONE)) { - fpW = fpH = grid.getSize() * fpS * zoom; // all gridless are relative to the grid size - } else { - fpW = footprintBounds.getWidth() * fpS * zoom; - fpH = footprintBounds.getHeight() * fpS * zoom; - } double imgW = img.getWidth(); double imgH = img.getHeight(); - double sXY = token.getSizeScale(); - double sX = token.getScaleX(); - double sY = token.getScaleY(); - // scale to fit image inside footprint bounds using the dimension that needs the most scaling, - // i.e. lowest ratio - double imageFootprintRatio = 1; - if (token.getShape() == Token.TokenShape.FIGURE && grid.isIsometric()) { - // uses double footprint height - imageFootprintRatio = Math.min(fpW / imgW, fpH * 2 / imgH); + if (token.isSnapToScale()) { + TokenFootprint footprint = token.getFootprint(grid); + Rectangle2D footprintBounds = footprint.getBounds(grid); + // except gridless, this should be 1 for footprints larger than the grid + double fpS = footprint.getScale(); + double fpW, fpH; + // size: multiply by zoom level to prevent multiple scaling ops which lose definition, i.e. + // scale once + if (GridFactory.getGridType(grid).equals(GridFactory.NONE)) { + fpW = fpH = grid.getSize() * fpS * zoom; // all gridless are relative to the grid size + } else { + fpW = footprintBounds.getWidth() * fpS * zoom; + fpH = footprintBounds.getHeight() * fpS * zoom; + } + double sXY = token.getSizeScale(); + double sX = token.getScaleX(); + double sY = token.getScaleY(); + // scale to fit image inside footprint bounds using the dimension that needs the most scaling, + // i.e. lowest ratio + double imageFootprintRatio = 1; + if (token.getShape() == Token.TokenShape.FIGURE && grid.isIsometric()) { + // uses double footprint height + imageFootprintRatio = Math.min(fpW / imgW, fpH * 2 / imgH); + } else { + imageFootprintRatio = Math.min(fpW / imgW, fpH / imgH); + } + // combine with token scale properties + if (sX != 1 || sY != 1 || sXY != 1 || imageFootprintRatio != 1) { + int outputWidth = (int) Math.ceil(imgW * sXY * sX * imageFootprintRatio); + int outputHeight = (int) Math.ceil(imgH * sXY * sY * imageFootprintRatio); + token.setWidth(outputWidth); + token.setHeight(outputHeight); + try { + return ImageUtil.scaleBufferedImage(img, outputWidth, outputHeight); + } catch (Exception e) { + log.debug(e.getLocalizedMessage(), e); + return img; + } + } } else { - imageFootprintRatio = Math.min(fpW / imgW, fpH / imgH); - } - // combine with token scale properties - if (sX != 1 || sY != 1 || sXY != 1 || imageFootprintRatio != 1) { - int outputWidth = (int) Math.ceil(imgW * sXY * sX * imageFootprintRatio); - int outputHeight = (int) Math.ceil(imgH * sXY * sY * imageFootprintRatio); - token.setWidth(outputWidth); - token.setHeight(outputHeight); + Rectangle b = token.getBounds(grid.getZone()); try { - return ImageUtil.scaleBufferedImage(img, outputWidth, outputHeight); + return ImageUtil.scaleBufferedImage( + img, (int) Math.ceil(b.width * zoom), (int) Math.ceil(b.height * zoom)); } catch (Exception e) { log.debug(e.getLocalizedMessage(), e); return img; } - } else { - return img; } + return img; // fallback, return original } /** @@ -181,6 +191,132 @@ public static BufferedImage createCompatibleImage(Image img) { return createCompatibleImage(img, null); } + public static BufferedImage createCompatibleImage(Image img, Map hints) { + if (img == null) { + return null; + } + return createCompatibleImage(img, img.getWidth(null), img.getHeight(null), hints); + } + + /** + * Create a copy of the image that is compatible with the current graphics context and scaled to + * the supplied size + * + * @param img the image to copy + * @param width width of the created image + * @param height height of the created image + * @param hints a {@link Map} that may contain the key HINT_TRANSPARENCY to define a the + * transparency color + * @return a {@link BufferedImage} with a copy of img + */ + public static BufferedImage createCompatibleImage( + Image img, int width, int height, Map hints) { + width = Math.max(width, 1); + height = Math.max(height, 1); + + int transparency; + if (hints != null && hints.containsKey(HINT_TRANSPARENCY)) { + transparency = (Integer) hints.get(HINT_TRANSPARENCY); + } else { + transparency = pickBestTransparency(img); + } + BufferedImage compImg = new BufferedImage(width, height, transparency); + + Graphics2D g = null; + try { + g = compImg.createGraphics(); + AppPreferences.renderQuality.get().setRenderingHints(g); + g.drawImage(img, 0, 0, width, height, null); + } finally { + if (g != null) { + g.dispose(); + } + } + return compImg; + } + + /** + * Look at the image and determine which Transparency is most appropriate. If it finds any + * translucent pixels it returns Transparency.TRANSLUCENT, if it finds at least one purely + * transparent pixel and no translucent pixels it will return Transparency.BITMASK, in all other + * cases it returns Transparency.OPAQUE, including errors + * + * @param image to pick transparency from + * @return one of Transparency constants + */ + public static int pickBestTransparency(Image image) { + // Take a shortcut if possible + if (image instanceof BufferedImage) { + return pickBestTransparency((BufferedImage) image); + } + + // Legacy method + // NOTE: This is a horrible memory hog + int width = image.getWidth(null); + int height = image.getHeight(null); + int[] pixelArray = new int[width * height]; + PixelGrabber pg = new PixelGrabber(image, 0, 0, width, height, pixelArray, 0, width); + try { + pg.grabPixels(); + } catch (InterruptedException e) { + System.err.println("interrupted waiting for pixels!"); + return Transparency.OPAQUE; + } + if ((pg.getStatus() & ImageObserver.ABORT) != 0) { + log.error("image fetch aborted or errored"); + System.err.println("image fetch aborted or errored"); + return Transparency.OPAQUE; + } + // Look for specific pixels + boolean foundTransparent = false; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Get the next pixel + int pixel = pixelArray[y * width + x]; + int alpha = (pixel >> 24) & 0xff; + + // Is there translucency or just pure transparency ? + if (alpha > 0 && alpha < 255) { + return Transparency.TRANSLUCENT; + } + if (alpha == 0 && !foundTransparent) { + foundTransparent = true; + } + } + } + return foundTransparent ? Transparency.BITMASK : Transparency.OPAQUE; + } + + public static int pickBestTransparency(BufferedImage image) { + // See if we can short circuit + ColorModel colorModel = image.getColorModel(); + if (colorModel.getTransparency() == Transparency.OPAQUE) { + return Transparency.OPAQUE; + } + // Get the pixels + int width = image.getWidth(); + int height = image.getHeight(); + + // Look for specific pixels + boolean foundTransparent = false; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Get the next pixel + int pixel = image.getRGB(x, y); + int alpha = (pixel >> 24) & 0xff; + + // Is there translucency or just pure transparency ? + if (alpha > 0 && alpha < 255) { + return Transparency.TRANSLUCENT; + } + if (alpha == 0 && !foundTransparent) { + foundTransparent = true; + } + } + } + return foundTransparent ? Transparency.BITMASK : Transparency.OPAQUE; + } + public static BufferedImage createCompatibleImage(int width, int height, int transparency) { return new BufferedImage(width, height, transparency); } @@ -296,6 +432,36 @@ public static BufferedImage flipTokenImage(BufferedImage image, Token token) { } } + /** + * Flip the image and return a new image + * + * @param image the image to flip + * @param direction 0-nothing, 1-horizontal, 2-vertical, 3-both + * @return flipped BufferedImage + */ + public static BufferedImage flipCartesian(BufferedImage image, int direction) { + boolean flipHorizontal = (direction & 1) == 1; + boolean flipVertical = (direction & 2) == 2; + + if (!flipHorizontal && !flipVertical) { + return image; + } + + BufferedImage workImage = + new BufferedImage(image.getWidth(), image.getHeight(), image.getTransparency()); + + int workW = image.getWidth() * (flipHorizontal ? -1 : 1); + int workH = image.getHeight() * (flipVertical ? -1 : 1); + int workX = flipHorizontal ? image.getWidth() : 0; + int workY = flipVertical ? image.getHeight() : 0; + + Graphics2D wig = workImage.createGraphics(); + wig.drawImage(image, workX, workY, workW, workH, null); + wig.dispose(); + + return workImage; + } + /** * Load the image. Does not create a graphics configuration compatible version. * @@ -350,13 +516,6 @@ public static BufferedImage getCompatibleImage(String image, Map return createCompatibleImage(getImage(image), hints); } - public static BufferedImage createCompatibleImage(Image img, Map hints) { - if (img == null) { - return null; - } - return createCompatibleImage(img, img.getWidth(null), img.getHeight(null), hints); - } - /** * Load the image in the classpath. Does not create a graphics configuration compatible version. * @@ -379,125 +538,6 @@ public static Image getImage(String image) throws IOException { return bytesToImage(dataStream.toByteArray(), image); } - /** - * Create a copy of the image that is compatible with the current graphics context and scaled to - * the supplied size - * - * @param img the image to copy - * @param width width of the created image - * @param height height of the created image - * @param hints a {@link Map} that may contain the key HINT_TRANSPARENCY to define a the - * transparency color - * @return a {@link BufferedImage} with a copy of img - */ - public static BufferedImage createCompatibleImage( - Image img, int width, int height, Map hints) { - width = Math.max(width, 1); - height = Math.max(height, 1); - - int transparency; - if (hints != null && hints.containsKey(HINT_TRANSPARENCY)) { - transparency = (Integer) hints.get(HINT_TRANSPARENCY); - } else { - transparency = pickBestTransparency(img); - } - BufferedImage compImg = new BufferedImage(width, height, transparency); - - Graphics2D g = null; - try { - g = compImg.createGraphics(); - AppPreferences.renderQuality.get().setRenderingHints(g); - g.drawImage(img, 0, 0, width, height, null); - } finally { - if (g != null) { - g.dispose(); - } - } - return compImg; - } - - /** - * Look at the image and determine which Transparency is most appropriate. If it finds any - * translucent pixels it returns Transparency.TRANSLUCENT, if it finds at least one purely - * transparent pixel and no translucent pixels it will return Transparency.BITMASK, in all other - * cases it returns Transparency.OPAQUE, including errors - * - * @param image to pick transparency from - * @return one of Transparency constants - */ - public static int pickBestTransparency(Image image) { - // Take a shortcut if possible - if (image instanceof BufferedImage) { - return pickBestTransparency((BufferedImage) image); - } - - // Legacy method - // NOTE: This is a horrible memory hog - int width = image.getWidth(null); - int height = image.getHeight(null); - int[] pixelArray = new int[width * height]; - PixelGrabber pg = new PixelGrabber(image, 0, 0, width, height, pixelArray, 0, width); - try { - pg.grabPixels(); - } catch (InterruptedException e) { - System.err.println("interrupted waiting for pixels!"); - return Transparency.OPAQUE; - } - if ((pg.getStatus() & ImageObserver.ABORT) != 0) { - log.error("image fetch aborted or errored"); - System.err.println("image fetch aborted or errored"); - return Transparency.OPAQUE; - } - // Look for specific pixels - boolean foundTransparent = false; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // Get the next pixel - int pixel = pixelArray[y * width + x]; - int alpha = (pixel >> 24) & 0xff; - - // Is there translucency or just pure transparency ? - if (alpha > 0 && alpha < 255) { - return Transparency.TRANSLUCENT; - } - if (alpha == 0 && !foundTransparent) { - foundTransparent = true; - } - } - } - return foundTransparent ? Transparency.BITMASK : Transparency.OPAQUE; - } - - public static int pickBestTransparency(BufferedImage image) { - // See if we can short circuit - ColorModel colorModel = image.getColorModel(); - if (colorModel.getTransparency() == Transparency.OPAQUE) { - return Transparency.OPAQUE; - } - // Get the pixels - int width = image.getWidth(); - int height = image.getHeight(); - - // Look for specific pixels - boolean foundTransparent = false; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // Get the next pixel - int pixel = image.getRGB(x, y); - int alpha = (pixel >> 24) & 0xff; - - // Is there translucency or just pure transparency ? - if (alpha > 0 && alpha < 255) { - return Transparency.TRANSLUCENT; - } - if (alpha == 0 && !foundTransparent) { - foundTransparent = true; - } - } - } - return foundTransparent ? Transparency.BITMASK : Transparency.OPAQUE; - } - public static double getIsoFigureHeightOffset(Token token, Rectangle2D footprintBounds) { if (token.getShape().equals(Token.TokenShape.FIGURE) && !token.isFlippedIso()) { double imageFitRatio = getIsoFigureScaleFactor(token, footprintBounds); @@ -580,36 +620,6 @@ public static BufferedImage getTokenImage(Token token, ZoneRenderer zr) { return image; } - /** - * Flip the image and return a new image - * - * @param image the image to flip - * @param direction 0-nothing, 1-horizontal, 2-vertical, 3-both - * @return flipped BufferedImage - */ - public static BufferedImage flipCartesian(BufferedImage image, int direction) { - boolean flipHorizontal = (direction & 1) == 1; - boolean flipVertical = (direction & 2) == 2; - - if (!flipHorizontal && !flipVertical) { - return image; - } - - BufferedImage workImage = - new BufferedImage(image.getWidth(), image.getHeight(), image.getTransparency()); - - int workW = image.getWidth() * (flipHorizontal ? -1 : 1); - int workH = image.getHeight() * (flipVertical ? -1 : 1); - int workX = flipHorizontal ? image.getWidth() : 0; - int workY = flipVertical ? image.getHeight() : 0; - - Graphics2D wig = workImage.createGraphics(); - wig.drawImage(image, workX, workY, workW, workH, null); - wig.dispose(); - - return workImage; - } - public static BufferedImage flipIsometric(BufferedImage image, boolean toRhombus) { BufferedImage workImage; boolean isSquished = From 0b9af49be8647459d49450dab2fb30f4fef35bae Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:53:30 +0800 Subject: [PATCH 23/24] cleanup and spotless --- .../java/net/rptools/lib/image/ImageUtil.java | 20 ++++++------------- .../renderer/tokenRender/TokenRenderer.java | 14 ++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index 0e8164a23c..c61a6c4f95 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -49,15 +49,8 @@ public class ImageUtil { return Arrays.asList(ImageIO.getReaderFileSuffixes()).contains(name); }; - // TODO: perhaps look at reintroducing this later - // private static GraphicsConfiguration graphicsConfig = - // GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); private static final Logger log = LogManager.getLogger(); - // public static void setGraphicsConfiguration(GraphicsConfiguration config) { - // graphicsConfig = config; - // } - // private static final JPanel observer = new JPanel(); private static final int[][] outlineNeighborMap = { {0, -1, 100}, // N @@ -107,8 +100,7 @@ public static int negativeColourInt(int rgb) { int r = 255 - ((rgb >> 16) & 0xFF); int g = 255 - ((rgb >> 8) & 0xFF); int b = 255 - (rgb & 0xFF); - int negativeRGB = (r << 16) | (g << 8) | b; - return negativeRGB; + return (r << 16) | (g << 8) | b; } /** @@ -148,7 +140,7 @@ public static BufferedImage getScaledTokenImage( double sY = token.getScaleY(); // scale to fit image inside footprint bounds using the dimension that needs the most scaling, // i.e. lowest ratio - double imageFootprintRatio = 1; + double imageFootprintRatio; if (token.getShape() == Token.TokenShape.FIGURE && grid.isIsometric()) { // uses double footprint height imageFootprintRatio = Math.min(fpW / imgW, fpH * 2 / imgH); @@ -423,7 +415,7 @@ public static BufferedImage createOutline(BufferedImage sourceImage, Color color * @return A modified image, or the original image if no processing performed. */ public static BufferedImage flipTokenImage(BufferedImage image, Token token) { - int direction = 0 + (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); + int direction = (token.isFlippedX() ? 1 : 0) + (token.isFlippedY() ? 2 : 0); image = flipCartesian(image, direction); if (token.isFlippedIso()) { return IsometricGrid.isoImage(image); @@ -550,8 +542,8 @@ public static double getIsoFigureHeightOffset(Token token, Rectangle2D footprint /** * Use width ratio unless height exceeds double footprint height * - * @param token - * @param footprintBounds + * @param token Token + * @param footprintBounds Rectangle * @return double */ public static double getIsoFigureScaleFactor(Token token, Rectangle2D footprintBounds) { @@ -626,7 +618,7 @@ public static BufferedImage flipIsometric(BufferedImage image, boolean toRhombus MathUtil.inTolerance(image.getHeight(), image.getWidth() / 2d, image.getHeight() * 0.05); if (image.getWidth() != image.getHeight()) { int maxDim = Math.max(image.getWidth(), image.getHeight()); - int w = 1, h = 1; + int w, h = 1; if (toRhombus) { // make it square and centred w = h = maxDim; 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 43fad8e1c9..a8088b4d77 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 @@ -45,11 +45,9 @@ public class TokenRenderer { private float opacity = 1f; private Area clip = null; private ZoneRenderer renderer; - private Zone zone; - private Grid grid; + private Grid grid; private double scale; - private boolean isSquare = false; - private boolean isoFigure = false; + private boolean isoFigure = false; private boolean canSpin = false; private BufferedImage renderImage; private boolean initialised = false; @@ -68,10 +66,10 @@ public boolean isInitialised() { public void setRenderer(ZoneRenderer zoneRenderer) { timer.start("TokenRenderer-init"); renderer = zoneRenderer; - zone = renderer.getZone(); + Zone zone = renderer.getZone(); grid = zone.getGrid(); - isSquare = GridFactory.getGridType(grid).equals(GridFactory.SQUARE); - scale = renderer.getScale(); + + scale = renderer.getScale(); initialised = true; timer.stop("TokenRenderer-init"); } @@ -120,7 +118,7 @@ private void compareStates() { && !currentState.flippedIso && currentState.shape.equals(Token.TokenShape.FIGURE); canSpin = currentState.shape.equals(Token.TokenShape.TOP_DOWN); - boolean updateStoredImage = false; + boolean updateStoredImage; if (!tokenStateMap.containsKey(token)) { updateStoredImage = true; } else { From 0535e1d312ae17336251663774b9006d1270eabf Mon Sep 17 00:00:00 2001 From: bubblobill <45483160+bubblobill@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:57:49 +0800 Subject: [PATCH 24/24] cleanup and spotless --- src/main/java/net/rptools/lib/image/ImageUtil.java | 2 +- .../ui/zone/renderer/tokenRender/TokenRenderer.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/rptools/lib/image/ImageUtil.java b/src/main/java/net/rptools/lib/image/ImageUtil.java index c61a6c4f95..e55b7e3dd1 100644 --- a/src/main/java/net/rptools/lib/image/ImageUtil.java +++ b/src/main/java/net/rptools/lib/image/ImageUtil.java @@ -100,7 +100,7 @@ public static int negativeColourInt(int rgb) { int r = 255 - ((rgb >> 16) & 0xFF); int g = 255 - ((rgb >> 8) & 0xFF); int b = 255 - (rgb & 0xFF); - return (r << 16) | (g << 8) | b; + return (r << 16) | (g << 8) | b; } /** 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 a8088b4d77..b112f8eccd 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 @@ -45,9 +45,9 @@ public class TokenRenderer { private float opacity = 1f; private Area clip = null; private ZoneRenderer renderer; - private Grid grid; + private Grid grid; private double scale; - private boolean isoFigure = false; + private boolean isoFigure = false; private boolean canSpin = false; private BufferedImage renderImage; private boolean initialised = false; @@ -66,10 +66,10 @@ public boolean isInitialised() { public void setRenderer(ZoneRenderer zoneRenderer) { timer.start("TokenRenderer-init"); renderer = zoneRenderer; - Zone zone = renderer.getZone(); + Zone zone = renderer.getZone(); grid = zone.getGrid(); - scale = renderer.getScale(); + scale = renderer.getScale(); initialised = true; timer.stop("TokenRenderer-init"); }
%s