diff --git a/src/main/java/net/rptools/maptool/client/DeveloperOptions.java b/src/main/java/net/rptools/maptool/client/DeveloperOptions.java index 2d97e90d6d..90efc99e62 100644 --- a/src/main/java/net/rptools/maptool/client/DeveloperOptions.java +++ b/src/main/java/net/rptools/maptool/client/DeveloperOptions.java @@ -41,7 +41,12 @@ public enum Toggle { /** When enabled, recalculates the grid shape each time it is needed. */ IgnoreGridShapeCache("ignoreGridShapeCache"), - ; + + /** + * When enabled, highlights the important points used during token drags, for example, the drag + * anchor and starting position of the cursor. + */ + DebugTokenDragging("debugTokenDragging"); private final String key; diff --git a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java index f952e118c2..2649bdb65e 100644 --- a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java @@ -24,6 +24,7 @@ import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.text.AttributedCharacterIterator; import java.text.AttributedString; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.stream.Collectors; +import javax.annotation.Nullable; import javax.swing.*; import net.rptools.lib.CodeTimer; import net.rptools.lib.MD5Key; @@ -51,7 +53,6 @@ import net.rptools.maptool.model.*; import net.rptools.maptool.model.Pointer.Type; import net.rptools.maptool.model.Zone.VisionType; -import net.rptools.maptool.model.player.Player; import net.rptools.maptool.model.player.Player.Role; import net.rptools.maptool.model.sheet.stats.StatSheetManager; import net.rptools.maptool.util.GraphicsUtil; @@ -59,8 +60,6 @@ import net.rptools.maptool.util.StringUtil; import net.rptools.maptool.util.TokenUtil; import org.apache.commons.lang.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * This is the pointer tool from the top-level of the toolbar. It allows tokens to be selected and @@ -70,16 +69,13 @@ */ public class PointerTool extends DefaultTool { private static final long serialVersionUID = 8606021718606275084L; - private static final Logger log = LogManager.getLogger(PointerTool.class); private BufferedImage panelTexture = RessourceManager.getImage(Images.TEXTURE_PANEL); private boolean isShowingTokenStackPopup; private boolean isShowingPointer; - private boolean isDraggingToken; private boolean isNewTokenSelected; private boolean isDrawingSelectionBox; private boolean isSpaceDown; - private boolean isMovingWithKeys; private Rectangle selectionBoundBox; // Hovers @@ -90,7 +86,6 @@ public class PointerTool extends DefaultTool { // Track token interactions to hide statsheets when doing other stuff private boolean mouseButtonDown = false; - private Token tokenBeingDragged; private Token tokenUnderMouse; private Token markerUnderMouse; private int keysDown; // used to record whether Shift/Ctrl/Meta keys are down @@ -105,12 +100,13 @@ public class PointerTool extends DefaultTool { private static int PADDING = 7; private static int STATSHEET_EXTERIOR_PADDING = 5; - // Offset from token's X,Y when dragging. Values are in zone coordinates. - private int dragOffsetX = 0; - private int dragOffsetY = 0; + // Keeps track of the start of a token drag, in screen coordinates. Useful for drawing selection + // boxes. private int dragStartX = 0; private int dragStartY = 0; + private @Nullable TokenDragOp tokenDragOp; + private String currentPointerName; public PointerTool() { @@ -208,86 +204,33 @@ public String getTooltip() { } public void startTokenDrag(Token keyToken, Set tokens) { - tokenBeingDragged = keyToken; + startTokenDrag( + keyToken, tokens, new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), false); + } - Player p = MapTool.getPlayer(); - if (!p.isGM() + private void startTokenDrag( + Token keyToken, Set tokens, ZonePoint dragStart, boolean isMovingWithKeys) { + if (!MapTool.getPlayer().isGM() && (MapTool.getServerPolicy().isMovementLocked() || MapTool.getFrame().getInitiativePanel().isMovementLocked(keyToken))) { // Not allowed return; } - renderer.addMoveSelectionSet( - p.getName(), tokenBeingDragged.getId(), renderer.getOwnedTokens(tokens)); + tokens = renderer.getOwnedTokens(tokens); + renderer.addMoveSelectionSet(MapTool.getPlayer().getName(), keyToken.getId(), tokens); MapTool.serverCommand() .startTokenMove( - p.getName(), - renderer.getZone().getId(), - tokenBeingDragged.getId(), - renderer.getOwnedTokens(tokens)); + MapTool.getPlayer().getName(), renderer.getZone().getId(), keyToken.getId(), tokens); - isDraggingToken = true; + tokenDragOp = new TokenDragOp(renderer, keyToken, dragStart, isMovingWithKeys); } /** Complete the drag of the token, and expose FOW */ public void stopTokenDrag() { - renderer.commitMoveSelectionSet(tokenBeingDragged.getId()); // TODO: figure out a better way - isDraggingToken = false; - isMovingWithKeys = false; - - dragOffsetX = 0; - dragOffsetY = 0; - - exposeFoW(null); - } - - /** - * Expose the FoW at a ZonePoint, or at the visible area, for the selected token - * - * @param p the ZonePoint to expose, or a null if exposing visible area and last path - */ - public void exposeFoW(ZonePoint p) { - // if has fog(required) - // and ((isGM with pref set) OR serverPolicy allows auto reveal by players) - - String name = MapTool.getPlayer().getName(); - boolean isGM = MapTool.getPlayer().isGM(); - boolean ownerReveal; // if true, reveal FoW if current player owns the token. - boolean hasOwnerReveal; // if true, reveal FoW if token has an owner. - boolean noOwnerReveal; // if true, reveal FoW if token has no owners. - - if (MapTool.isPersonalServer()) { - ownerReveal = - hasOwnerReveal = noOwnerReveal = AppPreferences.getAutoRevealVisionOnGMMovement(); - } else { - ownerReveal = MapTool.getServerPolicy().isAutoRevealOnMovement(); - hasOwnerReveal = isGM && MapTool.getServerPolicy().isAutoRevealOnMovement(); - noOwnerReveal = isGM && MapTool.getServerPolicy().getGmRevealsVisionForUnownedTokens(); - } - if (renderer.getZone().hasFog() && (ownerReveal || hasOwnerReveal || noOwnerReveal)) { - Set exposeSet = new HashSet(); - Zone zone = renderer.getZone(); - for (GUID tokenGUID : renderer.getOwnedTokens(renderer.getSelectedTokenSet())) { - Token token = zone.getToken(tokenGUID); - if (token == null) { - continue; - } - if (ownerReveal && token.isOwner(name)) exposeSet.add(tokenGUID); - else if (hasOwnerReveal && token.hasOwners()) exposeSet.add(tokenGUID); - else if (noOwnerReveal && !token.hasOwners()) exposeSet.add(tokenGUID); - } - - if (p != null) { - FogUtil.exposeVisibleAreaAtWaypoint(renderer, exposeSet, p); - return; - } - - // Lee: fog exposure according to reveal type - if (!zone.getWaypointExposureToggle()) { - FogUtil.exposeLastPath(renderer, exposeSet); - } - FogUtil.exposeVisibleArea(renderer, exposeSet, false); + if (tokenDragOp != null) { + tokenDragOp.finish(); + tokenDragOp = null; } } @@ -366,7 +309,14 @@ public void handleMouseMotionEvent(MouseEvent event) { return; } tokenUnderMouse = token; - ((PointerTool) tool).startTokenDrag(token, Collections.singleton(token.getId())); + ((PointerTool) tool) + .startTokenDrag( + token, + Collections.singleton(token.getId()), + // TODO is dragstart even correct in this case? I know it's not from the map + // explorer + new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), + false); } } @@ -465,7 +415,7 @@ public void mousePressed(MouseEvent e) { if (isDraggingMap()) { return; } - if (isDraggingToken) { + if (tokenDragOp != null) { return; } dragStartX = e.getX(); // These same two lines are in super.mousePressed(). Why do them @@ -495,7 +445,7 @@ public void mousePressed(MouseEvent e) { // SELECTION Token token = renderer.getTokenAt(e.getX(), e.getY()); final var selectionModel = renderer.getSelectionModel(); - if (token != null && !isDraggingToken && SwingUtilities.isLeftMouseButton(e)) { + if (token != null && tokenDragOp == null && SwingUtilities.isLeftMouseButton(e)) { // Don't select if it's already being moved by someone isNewTokenSelected = false; if (!renderer.isTokenMoving(token)) { @@ -512,15 +462,6 @@ public void mousePressed(MouseEvent e) { isNewTokenSelected = true; selectionModel.replaceSelection(Collections.singletonList(token.getId())); } - // ZonePoint dragged to - ZonePoint pos = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer); - - // Offset specific to the token - Point tokenOffset = token.getDragOffset(getZone()); - - // Dragging offset for currently selected token - dragOffsetX = pos.x - tokenOffset.x; - dragOffsetY = pos.y - tokenOffset.y; } } else { if (SwingUtilities.isLeftMouseButton(e)) { @@ -553,8 +494,8 @@ public void mouseReleased(MouseEvent e) { // Jamz: We have to capture here as isLeftMouseButton is also true during drag // Jamz: Also, changed to right button which is easier to click during drag // WAYPOINT - if (SwingUtilities.isRightMouseButton(e) && isDraggingToken) { - setWaypoint(); + if (SwingUtilities.isRightMouseButton(e) && tokenDragOp != null) { + tokenDragOp.setWaypoint(); setDraggingMap(false); // We no longer drag the map. Fixes bug #616 return; } @@ -568,7 +509,7 @@ public void mouseReleased(MouseEvent e) { if (tokenUnderMouse == null && markerUnderMouse != null && !isShowingHover - && !isDraggingToken) { + && tokenDragOp == null) { isShowingHover = true; hoverTokenBounds = renderer.getMarkerBounds(markerUnderMouse); hoverTokenNotes = createHoverNote(markerUnderMouse); @@ -593,7 +534,7 @@ public void mouseReleased(MouseEvent e) { return; } // DRAG TOKEN COMPLETE - if (isDraggingToken) { + if (tokenDragOp != null) { SwingUtil.showPointer(renderer); stopTokenDrag(); } else { @@ -610,7 +551,7 @@ public void mouseReleased(MouseEvent e) { } } } finally { - isDraggingToken = false; + tokenDragOp = null; isDrawingSelectionBox = false; } return; @@ -621,12 +562,12 @@ public void mouseReleased(MouseEvent e) { // And Middle button? That's a pain to click while dragging isn't it? How about Right click // during drag? // WAYPOINT - if (SwingUtilities.isMiddleMouseButton(e) && isDraggingToken) { - setWaypoint(); + if (SwingUtilities.isMiddleMouseButton(e) && tokenDragOp != null) { + tokenDragOp.setWaypoint(); } // POPUP MENU - if (SwingUtilities.isRightMouseButton(e) && !isDraggingToken && !isDraggingMap()) { + if (SwingUtilities.isRightMouseButton(e) && tokenDragOp == null && !isDraggingMap()) { final var selectionModel = renderer.getSelectionModel(); if (tokenUnderMouse != null && !selectionModel.isSelected(tokenUnderMouse.getId())) { if (!SwingUtil.isShiftDown(e)) { @@ -690,22 +631,10 @@ public void mouseMoved(MouseEvent e) { return; } - if (isDraggingToken) { - // FJE If we're dragging the token, wouldn't mouseDragged() be called instead? Can this - // code - // ever be executed? - if (isMovingWithKeys) { - return; - } - ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); - ZonePoint last; - if (tokenUnderMouse == null) last = zp; - else { - last = renderer.getLastWaypoint(tokenUnderMouse.getId()); - // XXX This shouldn't be possible, but it happens?! - if (last == null) last = zp; - } - handleDragToken(zp, zp.x - last.x, zp.y - last.y); + if (tokenDragOp != null) { + // Note that a token "drag" can be started from the context menu, so mouseMoved() is called, + // not mouseDragged(). + tokenDragOp.dragTo(mouseX, mouseY); return; } var oldTokenUnderMouse = tokenUnderMouse; @@ -806,39 +735,11 @@ public void mouseDragged(MouseEvent e) { || !renderer.getSelectedTokenSet().contains(tokenUnderMouse.getId())) { return; } - if (isDraggingToken) { - if (isMovingWithKeys) { - return; - } - ZonePoint last = renderer.getLastWaypoint(tokenUnderMouse.getId()); - if (last == null) { - // This makes no sense to me. Why create a fake last point that is - // half the token width away from the current point? (Phil) - // last = new ZonePoint( - // tokenUnderMouse.getX() + r.width / 2, - // tokenUnderMouse.getY() + r.height / 2); - - // Just make a last ZP that is the same. - last = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); - } - ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); - // These lines were causing tokens to end up in the wrong grid cell in - // relation to the the mouse location. - // if (tokenUnderMouse.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { - // zp.translate(-r.width / 2, -r.height / 2); - // last.translate(-r.width / 2, -r.height / 2); - // } - // zp.translate(-dragOffsetX, -dragOffsetY); - - // Now the dx/dy are calculated on Zone Points that haven't been - // translated for drag offset or for snapping. That is being done in - // handleDragToken(). - int dx = zp.x - last.x; - int dy = zp.y - last.y; - handleDragToken(zp, dx, dy); + if (tokenDragOp != null) { + tokenDragOp.dragTo(mouseX, mouseY); return; } - if (!isDraggingToken && renderer.isTokenMoving(tokenUnderMouse)) { + if (renderer.isTokenMoving(tokenUnderMouse)) { return; } if (isNewTokenSelected) { @@ -866,184 +767,20 @@ public void mouseDragged(MouseEvent e) { } } } - startTokenDrag(tokenUnderMouse, selectedTokenSet); - isDraggingToken = true; - if (AppPreferences.getHideMousePointerWhileDragging()) SwingUtil.hidePointer(renderer); + startTokenDrag( + tokenUnderMouse, + selectedTokenSet, + new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), + false); + if (AppPreferences.getHideMousePointerWhileDragging()) { + SwingUtil.hidePointer(renderer); + } } return; } super.mouseDragged(e); } - public boolean isDraggingToken() { - return isDraggingToken; - } - - /** - * Move the keytoken being dragged to this zone point - * - * @param zonePoint The new ZonePoint for the token. - * @param dx The amount being moved in the X direction - * @param dy The amount being moved in the Y direction - * @return true if the move was successful - */ - public boolean handleDragToken(ZonePoint zonePoint, int dx, int dy) { - Grid grid = renderer.getZone().getGrid(); - // Always correct for offset. Fix #1589 - zonePoint.translate(-dragOffsetX, -dragOffsetY); - // For snapped dragging - if (tokenBeingDragged.isSnapToGrid() - && grid.getCapabilities().isSnapToGridSupported() - && AppPreferences.getTokensSnapWhileDragging()) { - // Convert the zone point to a cell point and back to force the snap to grid on drag - zonePoint = grid.convert(grid.convert(zonePoint)); - } - CellPoint cellUnderMouse = grid.convert(zonePoint); - MapTool.getFrame().getCoordinateStatusBar().update(cellUnderMouse.x, cellUnderMouse.y); - // Don't bother if there isn't any movement - if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), zonePoint)) { - return false; - } - // Make sure it's a valid move - boolean isValid; - if (grid.getSize() >= 9) - isValid = validateMove(tokenBeingDragged, renderer.getSelectedTokenSet(), zonePoint, dx, dy); - else - isValid = validateMove_legacy(tokenBeingDragged, renderer.getSelectedTokenSet(), zonePoint); - - if (!isValid) { - return false; - } - dragStartX = zonePoint.x; - dragStartY = zonePoint.y; - - renderer.updateMoveSelectionSet(tokenBeingDragged.getId(), zonePoint); - MapTool.serverCommand() - .updateTokenMove( - renderer.getZone().getId(), tokenBeingDragged.getId(), zonePoint.x, zonePoint.y); - return true; - } - - private boolean validateMove( - Token leadToken, Set tokenSet, ZonePoint point, int dirx, int diry) { - if (MapTool.getPlayer().isGM()) { - return true; - } - boolean isBlocked = false; - Zone zone = renderer.getZone(); - if (zone.hasFog()) { - // Check that the new position for each token is within the exposed area - Area zoneFog = zone.getExposedArea(); - if (zoneFog == null) zoneFog = new Area(); - boolean useTokenExposedArea = - MapTool.getServerPolicy().isUseIndividualFOW() && zone.getVisionType() != VisionType.OFF; - int deltaX = point.x - leadToken.getX(); - int deltaY = point.y - leadToken.getY(); - Grid grid = zone.getGrid(); - // Loop through all tokens. As soon as one of them is blocked, stop processing and - // return - // false. - // Jamz: Option this for lead token only? It's annoying dragging a group when one token - // has - // limited vision... - // Or if ANY token in group can move, finish move? - for (Iterator iter = tokenSet.iterator(); !isBlocked && iter.hasNext(); ) { - Area tokenFog = new Area(zoneFog); - GUID tokenGUID = iter.next(); - Token token = zone.getToken(tokenGUID); - if (token == null) { - continue; - } - - // Rolled back change from commit 3d5f619 because of reported bug by dorpond - // https://github.com/JamzTheMan/maptool/commit/3d5f619dff6e61c605ee532ac3c86a3860e91864 - if (useTokenExposedArea) { - ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); - tokenFog.add(meta.getExposedAreaHistory()); - - // Jamz: Allow a token without site to move within the current PlayerView - if (!token.getHasSight()) { - tokenFog.add(renderer.getZoneView().getVisibleArea(new PlayerView(Role.PLAYER))); - } - } - - Rectangle tokenSize = token.getBounds(zone); - Rectangle destination = - new Rectangle( - tokenSize.x + deltaX, tokenSize.y + deltaY, tokenSize.width, tokenSize.height); - isBlocked = !grid.validateMove(token, destination, dirx, diry, tokenFog); - } - } - return !isBlocked; - } - - private boolean validateMove_legacy(Token leadToken, Set tokenSet, ZonePoint point) { - Zone zone = renderer.getZone(); - if (MapTool.getPlayer().isGM()) { - return true; - } - boolean isVisible = true; - if (zone.hasFog()) { - // Check that the new position for each token is within the exposed area - Area fow = zone.getExposedArea(); - if (fow == null) { - return true; - } - isVisible = false; - int fudgeSize = Math.max(Math.min((zone.getGrid().getSize() - 2) / 3 - 1, 8), 0); - int deltaX = point.x - leadToken.getX(); - int deltaY = point.y - leadToken.getY(); - Rectangle bounds = new Rectangle(); - for (GUID tokenGUID : tokenSet) { - Token token = zone.getToken(tokenGUID); - if (token == null) { - continue; - } - int x = token.getX() + deltaX; - int y = token.getY() + deltaY; - - Rectangle tokenSize = token.getBounds(zone); - /* - * Perhaps create a counter and count the number of times that the contains() check returns true? There are currently 9 rectangular areas checked by this code (note the "/3" in the two - * 'interval' variables) so checking for 5 or more would mean more than 55%+ of the destination was visible... - */ - int intervalX = tokenSize.width - fudgeSize * 2; - int intervalY = tokenSize.height - fudgeSize * 2; - int counter = 0; - for (int dy = 0; dy < 3; dy++) { - for (int dx = 0; dx < 3; dx++) { - int by = y + fudgeSize + (intervalY * dy / 3); - int bx = x + fudgeSize + (intervalX * dx / 3); - bounds.x = bx; - bounds.y = by; - bounds.width = intervalY * (dy + 1) / 3 - intervalY * dy / 3; // No, this - // isn't the - // same as - // intervalY*1/3 - // because of - // integer - // arithmetic - bounds.height = intervalX * (dx + 1) / 3 - intervalX * dx / 3; - - if (!MapTool.getServerPolicy().isUseIndividualFOW() - || zone.getVisionType() == VisionType.OFF) { - if (fow.contains(bounds)) { - counter++; - } - } else { - ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); - if (meta.getExposedAreaHistory().contains(bounds)) { - counter++; - } - } - } - } - isVisible = (counter >= 6); - } - } - return isVisible; - } - /** * @note These keystrokes are currently hard-coded and should be exported to a property file in a * perfect universe. :) @@ -1187,7 +924,7 @@ public void actionPerformed(ActionEvent e) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { - if (!isDraggingToken) { + if (tokenDragOp == null) { return; } // Stop @@ -1201,7 +938,7 @@ public void actionPerformed(ActionEvent e) { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { - if (!isDraggingToken) { + if (tokenDragOp == null) { return; } // Stop @@ -1388,11 +1125,11 @@ private void handleKeyRotate(int direction, boolean freeRotate) { * @param dy The Y movement in Cell units */ public void handleKeyMove(double dx, double dy) { - Token keyToken = null; - if (!isDraggingToken) { + if (tokenDragOp == null) { // Start Set selectedTokenSet = renderer.getOwnedTokens(renderer.getSelectedTokenSet()); + Token keyToken = null; for (GUID tokenId : selectedTokenSet) { Token token = renderer.getZone().getToken(tokenId); if (token == null) { @@ -1414,45 +1151,16 @@ public void handleKeyMove(double dx, double dy) { // Note these are zone space coordinates dragStartX = keyToken.getX(); dragStartY = keyToken.getY(); - startTokenDrag(keyToken, selectedTokenSet); - } - if (!isMovingWithKeys) { - dragOffsetX = 0; - dragOffsetY = 0; - } - // The zone point the token will be moved to after adjusting for dx/dy - ZonePoint zp = new ZonePoint(dragStartX, dragStartY); - Grid grid = renderer.getZone().getGrid(); - if (tokenBeingDragged.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { - CellPoint cp = grid.convert(zp); - cp.x += dx; - cp.y += dy; - zp = grid.convert(cp); - dx = zp.x - tokenBeingDragged.getX(); - dy = zp.y - tokenBeingDragged.getY(); - } else { - // Scalar for dx/dy in zone space. Defaulting to essentially 1 pixel. - int moveFactor = 1; - if (tokenBeingDragged.isSnapToGrid()) { - // Move in grid size increments. Allows tokens set snap-to-grid on gridless maps - // to move in whole cell size increments. - moveFactor = grid.getSize(); - } - int x = dragStartX + (int) (dx * moveFactor); - int y = dragStartY + (int) (dy * moveFactor); - zp = new ZonePoint(x, y); - } - isMovingWithKeys = true; - handleDragToken(zp, (int) dx, (int) dy); - } + startTokenDrag( + keyToken, selectedTokenSet, new ZonePoint(keyToken.getX(), keyToken.getY()), true); + } - private void setWaypoint() { - ZonePoint p = new ZonePoint(dragStartX, dragStartY); - exposeFoW(p); + if (tokenDragOp == null) { + // Typically would be set in startTokenDrag() above, but not if server policy prevents it. + return; + } - renderer.toggleMoveSelectionSetWaypoint(tokenBeingDragged.getId(), p); - MapTool.serverCommand() - .toggleTokenMoveWaypoint(renderer.getZone().getId(), tokenBeingDragged.getId(), p); + tokenDragOp.moveByKey(dx, dy); } // // @@ -1470,8 +1178,8 @@ public void actionPerformed(ActionEvent e) { if (isSpaceDown) { return; } - if (isDraggingToken) { - setWaypoint(); + if (tokenDragOp != null) { + tokenDragOp.setWaypoint(); } else { // Pointer isShowingPointer = true; @@ -1699,7 +1407,7 @@ public void paintOverlay(Graphics2D g) { } // Statsheet if (tokenUnderMouse != null - && !isDraggingToken + && tokenDragOp == null && AppUtil.tokenIsVisible( renderer.getZone(), tokenUnderMouse, new PlayerView(MapTool.getPlayer().getRole()))) { if (AppPreferences.getPortraitSize() > 0 @@ -1977,7 +1685,7 @@ && new StatSheetManager().isLegacyStatSheet(tokenUnderMouse.getStatSheet())) { } // Jamz: Statsheet was still showing on drag, added other tests to hide statsheet as well - if (statSheet != null && !isDraggingToken && !mouseButtonDown) { + if (statSheet != null && tokenDragOp == null && !mouseButtonDown) { g.drawImage( statSheet, STATSHEET_EXTERIOR_PADDING, @@ -2096,4 +1804,327 @@ private String createHoverNote(Token marker) { String hoverText = builder.toString(); return hoverText; } + + private static final class TokenDragOp { + private final ZoneRenderer renderer; + private final Token tokenBeingDragged; + private boolean isMovingWithKeys; + + private final ZonePoint dragAnchor; + // For snap-to-grid, the distance between the drag anchor and the snapped version of the drag + // anchor. + private final int snapOffsetX; + private final int snapOffsetY; + // Keeps track of the start and end of a token drag, in map coordinates. + // Useful for smoothly dragging tokens. + private final ZonePoint tokenDragStart; + private ZonePoint tokenDragCurrent; + + public TokenDragOp( + ZoneRenderer renderer, + Token tokenBeingDragged, + ZonePoint dragStart, + boolean isMovingWithKeys) { + this.renderer = renderer; + this.tokenBeingDragged = tokenBeingDragged; + this.isMovingWithKeys = isMovingWithKeys; + + // Drag offset is used to make the drag behave as if started at the token's drag point. + this.dragAnchor = tokenBeingDragged.getDragAnchor(renderer.getZone()); + this.snapOffsetX = dragAnchor.x - tokenBeingDragged.getX(); + this.snapOffsetY = dragAnchor.y - tokenBeingDragged.getY(); + + this.tokenDragStart = new ZonePoint(dragStart); + this.tokenDragCurrent = new ZonePoint(this.tokenDragStart); + } + + public void finish() { + renderer.commitMoveSelectionSet(tokenBeingDragged.getId()); // TODO: figure out a better way + exposeFoW(null); + } + + public void setWaypoint() { + var position = renderer.getKeyTokenDragAnchorPosition(tokenBeingDragged.getId()); + exposeFoW(position); + renderer.toggleMoveSelectionSetWaypoint(tokenBeingDragged.getId(), position); + MapTool.serverCommand() + .toggleTokenMoveWaypoint(renderer.getZone().getId(), tokenBeingDragged.getId(), position); + } + + public void dragTo(int mouseX, int mouseY) { + if (isMovingWithKeys) { + return; + } + + final boolean debugEnabled = DeveloperOptions.Toggle.DebugTokenDragging.isEnabled(); + + if (debugEnabled) { + renderer.setShape3( + new Rectangle2D.Double(tokenDragStart.x - 5, tokenDragStart.y - 5, 10, 10)); + renderer.setShape4(new Rectangle2D.Double(dragAnchor.x - 5, dragAnchor.y - 5, 10, 10)); + } + + ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); + + zonePoint.x = this.dragAnchor.x + zonePoint.x - tokenDragStart.x; + zonePoint.y = this.dragAnchor.y + zonePoint.y - tokenDragStart.y; + + var grid = renderer.getZone().getGrid(); + if (tokenBeingDragged.isSnapToGrid() + && grid.getCapabilities().isSnapToGridSupported() + && AppPreferences.getTokensSnapWhileDragging()) { + // Snap to grid point. + zonePoint = grid.convert(grid.convert(zonePoint)); + + if (debugEnabled) { + renderer.setShape(new Rectangle2D.Double(zonePoint.x - 5, zonePoint.y - 5, 10, 10)); + } + + // Adjust given offet from grid to anchor point. + zonePoint.x += this.snapOffsetX; + zonePoint.y += this.snapOffsetY; + } + + if (debugEnabled) { + renderer.setShape2(new Rectangle2D.Double(zonePoint.x - 5, zonePoint.y - 5, 10, 10)); + } + + @Nullable ZonePoint previous = renderer.getLastWaypoint(tokenBeingDragged.getId()); + if (previous == null) { + doDragTo(zonePoint, 0, 0); + } else { + doDragTo(zonePoint, zonePoint.x - previous.x, zonePoint.y - previous.y); + } + } + + public void moveByKey(double dx, double dy) { + isMovingWithKeys = true; + + ZonePoint zp; + Grid grid = renderer.getZone().getGrid(); + if (tokenBeingDragged.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { + CellPoint cp = grid.convert(tokenDragCurrent); + cp.x += (int) dx; + cp.y += (int) dy; + zp = grid.convert(cp); + + zp.x += snapOffsetX; + zp.y += snapOffsetY; + + dx = zp.x - tokenBeingDragged.getX(); + dy = zp.y - tokenBeingDragged.getY(); + } else { + // Scalar for dx/dy in zone space. Defaulting to essentially 1 pixel. + int moveFactor = 1; + if (tokenBeingDragged.isSnapToGrid()) { + // Move in grid size increments. Allows tokens set snap-to-grid on gridless maps + // to move in whole cell size increments. + moveFactor = grid.getSize(); + } + int x = tokenDragCurrent.x + (int) (dx * moveFactor); + int y = tokenDragCurrent.y + (int) (dy * moveFactor); + zp = new ZonePoint(x, y); + } + + doDragTo(zp, (int) dx, (int) dy); + } + + private void doDragTo(ZonePoint newAnchorPoint, int dirx, int diry) { + // Don't bother if there isn't any movement + if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), newAnchorPoint)) { + return; + } + + // Make sure it's a valid move + boolean isValid = + (renderer.getZone().getGrid().getSize() >= 9) + ? validateMove( + tokenBeingDragged, renderer.getSelectedTokenSet(), newAnchorPoint, dirx, diry) + : validateMove_legacy( + tokenBeingDragged, renderer.getSelectedTokenSet(), newAnchorPoint); + if (!isValid) { + return; + } + + tokenDragCurrent = new ZonePoint(newAnchorPoint); + + renderer.updateMoveSelectionSet(tokenBeingDragged.getId(), newAnchorPoint); + MapTool.serverCommand() + .updateTokenMove( + renderer.getZone().getId(), + tokenBeingDragged.getId(), + newAnchorPoint.x, + newAnchorPoint.y); + } + + /** + * Expose the FoW at a ZonePoint, or at the visible area, for the selected token + * + * @param p the ZonePoint to expose, or a null if exposing visible area and last path + */ + private void exposeFoW(ZonePoint p) { + // if has fog(required) + // and ((isGM with pref set) OR serverPolicy allows auto reveal by players) + + String name = MapTool.getPlayer().getName(); + boolean isGM = MapTool.getPlayer().isGM(); + boolean ownerReveal; // if true, reveal FoW if current player owns the token. + boolean hasOwnerReveal; // if true, reveal FoW if token has an owner. + boolean noOwnerReveal; // if true, reveal FoW if token has no owners. + + if (MapTool.isPersonalServer()) { + ownerReveal = + hasOwnerReveal = noOwnerReveal = AppPreferences.getAutoRevealVisionOnGMMovement(); + } else { + ownerReveal = MapTool.getServerPolicy().isAutoRevealOnMovement(); + hasOwnerReveal = isGM && MapTool.getServerPolicy().isAutoRevealOnMovement(); + noOwnerReveal = isGM && MapTool.getServerPolicy().getGmRevealsVisionForUnownedTokens(); + } + if (renderer.getZone().hasFog() && (ownerReveal || hasOwnerReveal || noOwnerReveal)) { + Set exposeSet = new HashSet(); + Zone zone = renderer.getZone(); + for (GUID tokenGUID : renderer.getOwnedTokens(renderer.getSelectedTokenSet())) { + Token token = zone.getToken(tokenGUID); + if (token == null) { + continue; + } + if (ownerReveal && token.isOwner(name)) exposeSet.add(tokenGUID); + else if (hasOwnerReveal && token.hasOwners()) exposeSet.add(tokenGUID); + else if (noOwnerReveal && !token.hasOwners()) exposeSet.add(tokenGUID); + } + + if (p != null) { + FogUtil.exposeVisibleAreaAtWaypoint(renderer, exposeSet, p); + return; + } + + // Lee: fog exposure according to reveal type + if (!zone.getWaypointExposureToggle()) { + FogUtil.exposeLastPath(renderer, exposeSet); + } + FogUtil.exposeVisibleArea(renderer, exposeSet, false); + } + } + + private boolean validateMove( + Token leadToken, Set tokenSet, ZonePoint point, int dirx, int diry) { + if (MapTool.getPlayer().isGM()) { + return true; + } + boolean isBlocked = false; + Zone zone = renderer.getZone(); + if (zone.hasFog()) { + // Check that the new position for each token is within the exposed area + Area zoneFog = zone.getExposedArea(); + if (zoneFog == null) zoneFog = new Area(); + boolean useTokenExposedArea = + MapTool.getServerPolicy().isUseIndividualFOW() + && zone.getVisionType() != VisionType.OFF; + int deltaX = point.x - leadToken.getX(); + int deltaY = point.y - leadToken.getY(); + Grid grid = zone.getGrid(); + // Loop through all tokens. As soon as one of them is blocked, stop processing and + // return + // false. + // Jamz: Option this for lead token only? It's annoying dragging a group when one token + // has + // limited vision... + // Or if ANY token in group can move, finish move? + for (Iterator iter = tokenSet.iterator(); !isBlocked && iter.hasNext(); ) { + Area tokenFog = new Area(zoneFog); + GUID tokenGUID = iter.next(); + Token token = zone.getToken(tokenGUID); + if (token == null) { + continue; + } + + // Rolled back change from commit 3d5f619 because of reported bug by dorpond + // https://github.com/JamzTheMan/maptool/commit/3d5f619dff6e61c605ee532ac3c86a3860e91864 + if (useTokenExposedArea) { + ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); + tokenFog.add(meta.getExposedAreaHistory()); + + // Jamz: Allow a token without site to move within the current PlayerView + if (!token.getHasSight()) { + tokenFog.add(renderer.getZoneView().getVisibleArea(new PlayerView(Role.PLAYER))); + } + } + + Rectangle tokenSize = token.getBounds(zone); + Rectangle destination = + new Rectangle( + tokenSize.x + deltaX, tokenSize.y + deltaY, tokenSize.width, tokenSize.height); + isBlocked = !grid.validateMove(token, destination, dirx, diry, tokenFog); + } + } + return !isBlocked; + } + + private boolean validateMove_legacy(Token leadToken, Set tokenSet, ZonePoint point) { + Zone zone = renderer.getZone(); + if (MapTool.getPlayer().isGM()) { + return true; + } + boolean isVisible = true; + if (zone.hasFog()) { + // Check that the new position for each token is within the exposed area + Area fow = zone.getExposedArea(); + if (fow == null) { + return true; + } + isVisible = false; + int fudgeSize = Math.max(Math.min((zone.getGrid().getSize() - 2) / 3 - 1, 8), 0); + int deltaX = point.x - leadToken.getX(); + int deltaY = point.y - leadToken.getY(); + Rectangle bounds = new Rectangle(); + for (GUID tokenGUID : tokenSet) { + Token token = zone.getToken(tokenGUID); + if (token == null) { + continue; + } + int x = token.getX() + deltaX; + int y = token.getY() + deltaY; + + Rectangle tokenSize = token.getBounds(zone); + /* + * Perhaps create a counter and count the number of times that the contains() check returns true? There are currently 9 rectangular areas checked by this code (note the "/3" in the two + * 'interval' variables) so checking for 5 or more would mean more than 55%+ of the destination was visible... + */ + int intervalX = tokenSize.width - fudgeSize * 2; + int intervalY = tokenSize.height - fudgeSize * 2; + int counter = 0; + for (int dy = 0; dy < 3; dy++) { + for (int dx = 0; dx < 3; dx++) { + int by = y + fudgeSize + (intervalY * dy / 3); + int bx = x + fudgeSize + (intervalX * dx / 3); + bounds.x = bx; + bounds.y = by; + bounds.width = intervalY * (dy + 1) / 3 - intervalY * dy / 3; // No, this + // isn't the + // same as + // intervalY*1/3 + // because of + // integer + // arithmetic + bounds.height = intervalX * (dx + 1) / 3 - intervalX * dx / 3; + + if (!MapTool.getServerPolicy().isUseIndividualFOW() + || zone.getVisionType() == VisionType.OFF) { + if (fow.contains(bounds)) { + counter++; + } + } else { + ExposedAreaMetaData meta = zone.getExposedAreaMetaData(token.getExposedAreaGUID()); + if (meta.getExposedAreaHistory().contains(bounds)) { + counter++; + } + } + } + } + isVisible = (counter >= 6); + } + } + return isVisible; + } + } } 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 c85b374f20..187b0f6e77 100644 --- a/src/main/java/net/rptools/maptool/client/tool/StampTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/StampTool.java @@ -32,7 +32,7 @@ import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; -import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; @@ -40,7 +40,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; +import javax.annotation.Nullable; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JDialog; @@ -51,6 +53,7 @@ import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.AppStyle; import net.rptools.maptool.client.AppUtil; +import net.rptools.maptool.client.DeveloperOptions; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ScreenPoint; import net.rptools.maptool.client.swing.SwingUtil; @@ -71,33 +74,25 @@ public class StampTool extends DefaultTool implements ZoneOverlay { private boolean isShowingTokenStackPopup; - private boolean isDraggingToken; private boolean isNewTokenSelected; private boolean isDrawingSelectionBox; - private boolean isMovingWithKeys; - private boolean isResizingToken; - private boolean isResizingRotatedToken; private Rectangle selectionBoundBox; - // The position with greater than integer accuracy of a rotated stamp that is being resized. - private Point2D.Double preciseStampZonePoint; - private ZonePoint lastResizeZonePoint; - - private Token tokenBeingDragged; private Token tokenUnderMouse; - private Token tokenBeingResized; private final TokenStackPanel tokenStackPanel = new TokenStackPanel(); // private Map rotateBoundsMap = new HashMap(); private final Map resizeBoundsMap = new HashMap(); - // Offset from token's X,Y when dragging. Values are in cell coordinates. - private int dragOffsetX; - private int dragOffsetY; + // Keeps track of the start of a token drag, in screen coordinates. + // Useful for drawing selection boxes and resizing tokens. private int dragStartX; private int dragStartY; + private @Nullable TokenDragOp tokenDragOp; + private @Nullable TokenResizeOp tokenResizeOp; + private BufferedImage resizeImg = RessourceManager.getImage(Images.RESIZE); public StampTool() {} @@ -138,29 +133,22 @@ public String getTooltip() { } public void startTokenDrag(Token keyToken, Set tokens) { - tokenBeingDragged = keyToken; + startTokenDrag( + keyToken, tokens, new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), false); + } + private void startTokenDrag( + Token keyToken, Set tokens, ZonePoint dragStart, boolean isMovingWithKeys) { if (!MapTool.getPlayer().isGM() && MapTool.getServerPolicy().isMovementLocked()) { // Not allowed return; } - renderer.addMoveSelectionSet(MapTool.getPlayer().getName(), tokenBeingDragged.getId(), tokens); + renderer.addMoveSelectionSet(MapTool.getPlayer().getName(), keyToken.getId(), tokens); MapTool.serverCommand() .startTokenMove( - MapTool.getPlayer().getName(), - renderer.getZone().getId(), - tokenBeingDragged.getId(), - tokens); - isDraggingToken = true; - } - - public void stopTokenDrag() { - renderer.commitMoveSelectionSet(tokenBeingDragged.getId()); // TODO: figure out a better way - isDraggingToken = false; - isMovingWithKeys = false; + MapTool.getPlayer().getName(), renderer.getZone().getId(), keyToken.getId(), tokens); - dragOffsetX = 0; - dragOffsetY = 0; + tokenDragOp = new TokenDragOp(renderer, keyToken, dragStart, isMovingWithKeys); } /** @@ -230,7 +218,12 @@ public void handleMouseMotionEvent(MouseEvent event) { tokenUnderMouse = location.getToken(); ((StampTool) tool) .startTokenDrag( - location.getToken(), Collections.singleton(location.getToken().getId())); + location.getToken(), + Collections.singleton(location.getToken().getId()), + // TODO is dragstart even correct in this case? I know it's not from the map + // explorer + new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), + false); } return; } @@ -293,7 +286,7 @@ public void mousePressed(MouseEvent e) { return; } - if (isDraggingToken) { + if (tokenDragOp != null) { return; } @@ -304,14 +297,18 @@ public void mousePressed(MouseEvent e) { for (Entry entry : resizeBoundsMap.entrySet()) { Shape bounds = entry.getKey(); if (bounds.contains(dragStartX, dragStartY)) { - dragOffsetX = bounds.getBounds().x + bounds.getBounds().width - e.getX(); - dragOffsetY = bounds.getBounds().y + bounds.getBounds().height - e.getY(); - - isResizingToken = true; // The token being resized does not necessarily = tokenUnderMouse. If there is more then one // token under the mouse, the top token will be the tokenUnderMouse, but it is the selected // that is intended to be resized. - tokenBeingResized = entry.getValue(); + tokenResizeOp = + new TokenResizeOp( + renderer, + entry.getValue(), + dragStartX, + dragStartY, + bounds.getBounds().x + bounds.getBounds().width - e.getX(), + bounds.getBounds().y + bounds.getBounds().height - e.getY()); + return; } } @@ -341,7 +338,7 @@ public void mousePressed(MouseEvent e) { // SELECTION Token token = getTokenAt(e.getX(), e.getY()); if (token != null - && !isDraggingToken + && tokenDragOp == null && SwingUtilities.isLeftMouseButton(e) && !renderer.isAutoResizeStamp()) { // Permission @@ -368,14 +365,6 @@ public void mousePressed(MouseEvent e) { isNewTokenSelected = true; selectionModel.replaceSelection(Collections.singletonList(token.getId())); } - // Position on the zone of the click - ZonePoint pos = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer); - - // Offset specific to the token - Point tokenOffset = token.getDragOffset(getZone()); - - dragOffsetX = pos.x - tokenOffset.x; - dragOffsetY = pos.y - tokenOffset.y; } } else { if (SwingUtilities.isLeftMouseButton(e)) { @@ -402,12 +391,9 @@ public void mouseReleased(MouseEvent e) { } } - if (isResizingToken) { - renderer.flush(tokenBeingResized); - MapTool.serverCommand().putToken(renderer.getZone().getId(), tokenBeingResized); - isResizingToken = false; - isResizingRotatedToken = false; - tokenBeingResized = null; + if (tokenResizeOp != null) { + tokenResizeOp.finish(); + tokenResizeOp = null; return; } @@ -439,8 +425,9 @@ public void mouseReleased(MouseEvent e) { } // DRAG TOKEN COMPLETE - if (isDraggingToken) { - stopTokenDrag(); + if (tokenDragOp != null) { + tokenDragOp.finish(); + tokenDragOp = null; } else { // IF SELECTING MULTIPLE, SELECT SINGLE TOKEN if (!SwingUtil.isShiftDown(e)) { @@ -455,13 +442,13 @@ public void mouseReleased(MouseEvent e) { } } } finally { - isDraggingToken = false; + tokenDragOp = null; isDrawingSelectionBox = false; } return; } // POPUP MENU - if (SwingUtilities.isRightMouseButton(e) && !isDraggingToken && !isDraggingMap()) { + if (SwingUtilities.isRightMouseButton(e) && tokenDragOp == null && !isDraggingMap()) { final var selectionModel = renderer.getSelectionModel(); if (tokenUnderMouse != null && !selectionModel.isSelected(tokenUnderMouse.getId())) { if (!SwingUtil.isShiftDown(e)) { @@ -514,12 +501,8 @@ public void mouseMoved(MouseEvent e) { mouseX = e.getX(); mouseY = e.getY(); - if (isDraggingToken) { - if (isMovingWithKeys) { - return; - } - ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); - handleDragToken(zonePoint); + if (tokenDragOp != null) { + tokenDragOp.dragTo(mouseX, mouseY); return; } tokenUnderMouse = getTokenAt(mouseX, mouseY); @@ -527,10 +510,10 @@ public void mouseMoved(MouseEvent e) { } private Token getTokenAt(int x, int y) { - Token token = renderer.getTokenAt(mouseX, mouseY); + Token token = renderer.getTokenAt(x, y); if (token == null) { for (var entry : resizeBoundsMap.entrySet()) { - if (entry.getKey().contains(mouseX, mouseY)) { + if (entry.getKey().contains(x, y)) { token = entry.getValue(); } } @@ -538,14 +521,6 @@ private Token getTokenAt(int x, int y) { return token; } - private ScreenPoint getNearestVertex(ScreenPoint point) { - ZonePoint zp = point.convertToZone(renderer); - zp = renderer.getZone().getNearestVertex(zp); - return ScreenPoint.fromZonePoint(renderer, zp); - } - - ScreenPoint p = new ScreenPoint(0, 0); - @Override public void mouseDragged(MouseEvent e) { mouseX = e.getX(); @@ -577,124 +552,8 @@ public void mouseDragged(MouseEvent e) { } } - if (isResizingToken) { - // Fixing a bug here. Need to adjust for Anchor points - Jamz - int anchorX = (int) (tokenBeingResized.getAnchor().x * renderer.getScale()); - int anchorY = (int) (tokenBeingResized.getAnchor().y * renderer.getScale()); - ScreenPoint sp = - new ScreenPoint(mouseX + dragOffsetX - anchorX, mouseY + dragOffsetY - anchorY); - - BufferedImage image = ImageManager.getImage(tokenBeingResized.getImageAssetId()); - - if (SwingUtil.isControlDown(e)) { // snap size to grid - sp = getNearestVertex(sp); - } - boolean isRotated = - tokenBeingResized.hasFacing() - && tokenBeingResized.getShape() == Token.TokenShape.TOP_DOWN - && tokenBeingResized.getFacing() != -90; - if (!isRotated - && SwingUtil.isShiftDown(e)) { // lock aspect ratio -- broken for rotated images - ScreenPoint tokenPoint = - ScreenPoint.fromZonePoint(renderer, tokenBeingResized.getX(), tokenBeingResized.getY()); - - double ratio = image.getWidth() / (double) image.getHeight(); - int dx = (int) (sp.x - tokenPoint.x); - - sp.y = (int) (tokenPoint.y + (dx / ratio)); - } - ZonePoint zp = sp.convertToZone(renderer); - p = ScreenPoint.fromZonePoint(renderer, zp); - - // For snap-to-grid tokens (except background stamps) we anchor at the center of the token. - final var isSnapToGridAndAnchoredAtCenter = - tokenBeingResized.isSnapToGrid() - && tokenBeingResized.getLayer().anchorSnapToGridAtCenter(); - final var snapToGridMultiplier = isSnapToGridAndAnchoredAtCenter ? 2 : 1; - - int newWidth = Math.max(1, (zp.x - tokenBeingResized.getX()) * snapToGridMultiplier); - int newHeight = Math.max(1, (zp.y - tokenBeingResized.getY()) * snapToGridMultiplier); - - if (SwingUtil.isControlDown(e) && isSnapToGridAndAnchoredAtCenter) { - // Account for the 1/2 cell on each side of the stamp (since it's anchored in the center) - newWidth += renderer.getZone().getGrid().getSize(); - newHeight += renderer.getZone().getGrid().getSize(); - } - // take into account rotated stamps - if (isRotated) { - // if we are beginning a new resize, reset the resizing variables. - if (!isResizingRotatedToken) { - isResizingRotatedToken = true; - preciseStampZonePoint = - new Point2D.Double(tokenBeingResized.getX(), tokenBeingResized.getY()); - lastResizeZonePoint = new ZonePoint(zp.x, zp.y); - } - // theta is the rotation angle clockwise from the positive x-axis to compensate for the +ve - // y-axis - // pointing downwards in zone space and an unrotated token has facing of -90. - int theta = -tokenBeingResized.getFacing() - 90; - - // can't handle snap to grid with rotated token when resizing because they have to be able - // to nudge. - if (tokenBeingResized.isSnapToGrid()) { - tokenBeingResized.setSnapToGrid(false); - } - Rectangle footprintBounds = tokenBeingResized.getBounds(renderer.getZone()); - - // zp = mouse location - int changeX = (zp.x - lastResizeZonePoint.x) * snapToGridMultiplier; - int changeY = (zp.y - lastResizeZonePoint.y) * snapToGridMultiplier; - - double sinTheta = Math.sin(Math.toRadians(theta)); - double cosTheta = Math.cos(Math.toRadians(theta)); - - // Calculate change in the stamp's height and width. - // Sine terms are negated from the standard rotation transform because the direction of - // theta - // is reversed (theta rotates clockwise) - double dw = changeX * cosTheta + changeY * sinTheta; - double dh = -changeX * sinTheta + changeY * cosTheta; - - newWidth = (int) Math.max(1, footprintBounds.width + dw); - newHeight = (int) Math.max(1, footprintBounds.height + dh); - - // Move the stamp to compensate for a change in the stamp's rotation anchor - // so that the stamp stays fixed in place while being resized - - // change in stamp's rotation anchor due to resize - double dx = dw / 2; - double dy = dh / 2; - - // change in rotated stamp's anchor due to resize. currently only works perfectly for - // clockwise 0-90 - // needs fine tuning for the three other quadrants to prevent the stamp from creeping - double dxRot = dx * cosTheta - dy * sinTheta; - double dyRot = dx * sinTheta + dy * cosTheta; - - // Resizing a stamp automatically adjusts its rotation anchor point, so only consider the - // adjustment required due to the rotation. - double stampAdjustX = dxRot - dx; - double stampAdjustY = dyRot - dy; - - // prevent the stamp from moving around if a limit has been reached. - if (newWidth == 1 || newHeight == 1) { - newWidth = newWidth == 1 ? 1 : footprintBounds.width; - newHeight = newHeight == 1 ? 1 : footprintBounds.height; - } else { - // remembering the precise location prevents the stamp from drifting due to rounding to - // int - preciseStampZonePoint.x += stampAdjustX; - preciseStampZonePoint.y += stampAdjustY; - - lastResizeZonePoint = (ZonePoint) zp.clone(); - } - tokenBeingResized.setX((int) (preciseStampZonePoint.x)); - tokenBeingResized.setY((int) (preciseStampZonePoint.y)); - } - tokenBeingResized.setScaleX(newWidth / (double) image.getWidth()); - tokenBeingResized.setScaleY(newHeight / (double) image.getHeight()); - - renderer.repaint(); + if (tokenResizeOp != null) { + tokenResizeOp.dragTo(mouseX, mouseY, SwingUtil.isShiftDown(e), SwingUtil.isControlDown(e)); return; } @@ -720,12 +579,8 @@ public void mouseDragged(MouseEvent e) { return; } - if (isDraggingToken) { - if (isMovingWithKeys) { - return; - } - ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); - handleDragToken(zonePoint); + if (tokenDragOp != null) { + tokenDragOp.dragTo(mouseX, mouseY); return; } @@ -734,7 +589,7 @@ public void mouseDragged(MouseEvent e) { return; } - if (!isDraggingToken && renderer.isTokenMoving(tokenUnderMouse)) { + if (tokenDragOp == null && renderer.isTokenMoving(tokenUnderMouse)) { return; } @@ -764,12 +619,11 @@ public void mouseDragged(MouseEvent e) { } } } - Point origin = new Point(tokenUnderMouse.getX(), tokenUnderMouse.getY()); - - origin.translate(dragOffsetX, dragOffsetY); - - startTokenDrag(tokenUnderMouse, selectedTokenSet); - isDraggingToken = true; + startTokenDrag( + tokenUnderMouse, + selectedTokenSet, + new ScreenPoint(dragStartX, dragStartY).convertToZone(renderer), + false); SwingUtil.hidePointer(renderer); } @@ -779,41 +633,6 @@ public void mouseDragged(MouseEvent e) { super.mouseDragged(e); } - public boolean isDraggingToken() { - return isDraggingToken; - } - - /** - * Move the keytoken being dragged to this zone point - * - * @param zonePoint the zone point to move to - * @return true if the move was successful - */ - public boolean handleDragToken(ZonePoint zonePoint) { - // TODO: Optimize this (combine with calling code) - if (tokenBeingDragged.isSnapToGrid() - && getZone().getGrid().getCapabilities().isSnapToGridSupported()) { - zonePoint.translate(-dragOffsetX, -dragOffsetY); - CellPoint cellUnderMouse = renderer.getZone().getGrid().convert(zonePoint); - zonePoint = renderer.getZone().getGrid().convert(cellUnderMouse); - MapTool.getFrame().getCoordinateStatusBar().update(cellUnderMouse.x, cellUnderMouse.y); - } else { - zonePoint.translate(-dragOffsetX, -dragOffsetY); - } - // Don't bother if there isn't any movement - if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), zonePoint)) { - return false; - } - dragStartX = zonePoint.x; - dragStartY = zonePoint.y; - - renderer.updateMoveSelectionSet(tokenBeingDragged.getId(), zonePoint); - MapTool.serverCommand() - .updateTokenMove( - renderer.getZone().getId(), tokenBeingDragged.getId(), zonePoint.x, zonePoint.y); - return true; - } - @Override protected void installKeystrokes(Map actionMap) { super.installKeystrokes(actionMap); @@ -844,11 +663,12 @@ public void actionPerformed(ActionEvent e) { new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - if (!isDraggingToken) { + if (tokenDragOp == null) { return; } - // Stop - stopTokenDrag(); + + tokenDragOp.finish(); + tokenDragOp = null; } }); actionMap.put( @@ -856,11 +676,12 @@ public void actionPerformed(ActionEvent e) { new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - if (!isDraggingToken) { + if (tokenDragOp == null) { return; } - // Stop - stopTokenDrag(); + + tokenDragOp.finish(); + tokenDragOp = null; } }); @@ -1077,7 +898,7 @@ public void actionPerformed(ActionEvent e) { } private void handleKeyMove(int dx, int dy, boolean micro) { - if (!isDraggingToken) { + if (tokenDragOp == null) { // Start Set selectedTokenSet = renderer.getSelectedTokenSet(); if (selectedTokenSet.size() != 1) { @@ -1092,34 +913,21 @@ private void handleKeyMove(int dx, int dy, boolean micro) { if (renderer.isTokenMoving(token)) { return; } + dragStartX = token.getX(); dragStartY = token.getY(); - startTokenDrag(token, selectedTokenSet); + startTokenDrag(token, selectedTokenSet, new ZonePoint(token.getX(), token.getY()), true); } - if (!isMovingWithKeys) { - dragOffsetX = 0; - dragOffsetY = 0; - } - ZonePoint zp = null; - if (tokenBeingDragged.isSnapToGrid()) { - CellPoint cp = renderer.getZone().getGrid().convert(new ZonePoint(dragStartX, dragStartY)); - - cp.x += dx; - cp.y += dy; - zp = renderer.getZone().getGrid().convert(cp); - } else { - Rectangle tokenSize = tokenBeingDragged.getBounds(renderer.getZone()); - - int x = dragStartX + (micro ? dx : (tokenSize.width * dx)); - int y = dragStartY + (micro ? dy : (tokenSize.height * dy)); - - zp = new ZonePoint(x, y); + if (tokenDragOp == null) { + // Typically would be set in startTokenDrag() above, but not if server policy prevents it. + return; } - isMovingWithKeys = true; - handleDragToken(zp); - if (tokenBeingDragged.getLayer().oneStepKeyDrag()) { - stopTokenDrag(); + + tokenDragOp.moveByKey(dx, dy, micro); + if (tokenDragOp.tokenBeingDragged.getLayer().oneStepKeyDrag()) { + tokenDragOp.finish(); + tokenDragOp = null; } } @@ -1251,33 +1059,11 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { g.drawImage(resizeImg, at, renderer); } - - // g.setColor(Color.red); - // g.fillRect((int)(p.x-2), (int)(p.y-2), 4, 4); - // - // // Rotate - // int length = 35; - // int cx = bounds.x + bounds.width/2; - // int cy = bounds.y + bounds.height/2; - // int facing = token.getFacing() != null ? token.getFacing() : 0; - // - // int x = (int)(cx + Math.cos(Math.toRadians(facing)) * length); - // int y = (int)(cy - Math.sin(Math.toRadians(facing)) * length); - // - // Ellipse2D rotateBounds = new Ellipse2D.Float(x-5, y-5, 10, 10); - // rotateBoundsMap.put(rotateBounds, token); - // - // g.setColor(Color.black); - // g.drawLine(cx, cy, x, y); - // g.fill(rotateBounds); - // - // g.setColor(Color.gray); - // g.draw(rotateBounds); } } } - public void resizeStamp() { + private void resizeStamp() { if (tokenUnderMouse == null) { // Cancel action, didn't start/end the selection over the stamp JOptionPane.showMessageDialog( @@ -1359,19 +1145,227 @@ public void adjustAnchor(double scaleX, double scaleY) { tokenUnderMouse.setAnchor(-x, -y); } - public Point getAdjustedAnchor(double scaleX, double scaleY) { - ZonePoint selectionTr = - ScreenPoint.convertToZone(renderer, selectionBoundBox.getX(), selectionBoundBox.getY()); - int gridSize = renderer.getZone().getGrid().getSize(); - int tokenX = tokenUnderMouse.getX() + tokenUnderMouse.getAnchor().x; - int tokenY = tokenUnderMouse.getY() + tokenUnderMouse.getAnchor().y; + private static final class TokenDragOp { + private final ZoneRenderer renderer; + private final Token tokenBeingDragged; + private boolean isMovingWithKeys; + + private final ZonePoint dragAnchor; + // For snap-to-grid, the distance between the drag anchor and the snapped version of the drag + // anchor. + private final int snapOffsetX; + private final int snapOffsetY; + // Keeps track of the start and end of a token drag, in map coordinates. + // Useful for smoothly dragging tokens. + private final ZonePoint tokenDragStart; + private ZonePoint tokenDragCurrent; + + public TokenDragOp( + ZoneRenderer renderer, + Token tokenBeingDragged, + ZonePoint dragStart, + boolean isMovingWithKeys) { + this.renderer = renderer; + this.tokenBeingDragged = tokenBeingDragged; + this.isMovingWithKeys = isMovingWithKeys; + + // Drag offset is used to make the drag behave as if started at the token's drag point. + this.dragAnchor = tokenBeingDragged.getDragAnchor(renderer.getZone()); + this.snapOffsetX = dragAnchor.x - tokenBeingDragged.getX(); + this.snapOffsetY = dragAnchor.y - tokenBeingDragged.getY(); + + this.tokenDragStart = new ZonePoint(dragStart); + this.tokenDragCurrent = new ZonePoint(this.tokenDragStart); + } - int x = (int) ((selectionTr.x - tokenX) * scaleX); - x = x - (((x / gridSize) + 1) * gridSize); + public void finish() { + renderer.commitMoveSelectionSet(tokenBeingDragged.getId()); // TODO: figure out a better way + } - int y = (int) ((selectionTr.y - tokenY) * scaleY); - y = y - (((y / gridSize) + 1) * gridSize); + public void dragTo(int mouseX, int mouseY) { + if (isMovingWithKeys) { + return; + } + + final boolean debugEnabled = DeveloperOptions.Toggle.DebugTokenDragging.isEnabled(); + + if (debugEnabled) { + renderer.setShape3( + new Rectangle2D.Double(tokenDragStart.x - 5, tokenDragStart.y - 5, 10, 10)); + renderer.setShape4(new Rectangle2D.Double(dragAnchor.x - 5, dragAnchor.y - 5, 10, 10)); + } + + ZonePoint zonePoint = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); + + zonePoint.x = this.dragAnchor.x + zonePoint.x - tokenDragStart.x; + zonePoint.y = this.dragAnchor.y + zonePoint.y - tokenDragStart.y; + + var grid = renderer.getZone().getGrid(); + if (tokenBeingDragged.isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { + // Snap to grid point. + zonePoint = grid.convert(grid.convert(zonePoint)); + + if (debugEnabled) { + renderer.setShape(new Rectangle2D.Double(zonePoint.x - 5, zonePoint.y - 5, 10, 10)); + } + + // Adjust given offet from grid to anchor point. + zonePoint.x += this.snapOffsetX; + zonePoint.y += this.snapOffsetY; + } + + if (debugEnabled) { + renderer.setShape2(new Rectangle2D.Double(zonePoint.x - 5, zonePoint.y - 5, 10, 10)); + } + + doDragTo(zonePoint); + } + + public void moveByKey(int dx, int dy, boolean micro) { + isMovingWithKeys = true; + + ZonePoint zp; + if (tokenBeingDragged.isSnapToGrid()) { + var grid = renderer.getZone().getGrid(); + CellPoint cp = grid.convert(tokenDragCurrent); + cp.x += dx; + cp.y += dy; + zp = grid.convert(cp); + + zp.x += snapOffsetX; + zp.y += snapOffsetY; + } else { + Rectangle tokenSize = tokenBeingDragged.getBounds(renderer.getZone()); + int x = tokenDragCurrent.x + (micro ? dx : (tokenSize.width * dx)); + int y = tokenDragCurrent.y + (micro ? dy : (tokenSize.height * dy)); + zp = new ZonePoint(x, y); + } + + doDragTo(zp); + } + + private void doDragTo(ZonePoint newAnchorPoint) { + tokenDragCurrent = new ZonePoint(newAnchorPoint); - return new Point(-x, -y); + // Don't bother if there isn't any movement + if (!renderer.hasMoveSelectionSetMoved(tokenBeingDragged.getId(), newAnchorPoint)) { + return; + } + + renderer.updateMoveSelectionSet(tokenBeingDragged.getId(), newAnchorPoint); + MapTool.serverCommand() + .updateTokenMove( + renderer.getZone().getId(), + tokenBeingDragged.getId(), + newAnchorPoint.x, + newAnchorPoint.y); + } + } + + private record Vector2(double x, double y) { + public static Vector2 sub(ZonePoint lhs, ZonePoint rhs) { + return new Vector2(lhs.x - rhs.x, lhs.y - rhs.y); + } + + public double dot(Vector2 other) { + return x * other.x + y * other.y; + } + } + + private static final class TokenResizeOp { + private final int dragOffsetX; + private final int dragOffsetY; + private final ZoneRenderer renderer; + private final Token tokenBeingResized; + private final Vector2 down; + private final Vector2 right; + + // The position of the bottom-right corner of the token, assuming it is not rotated. + private final double originalScaleX; + private final double originalScaleY; + private final ZonePoint startDragReference; + + private final BufferedImage tokenImage; + + public TokenResizeOp( + ZoneRenderer renderer, + Token tokenBeingResized, + int dragStartX, + int dragStartY, + int dragOffsetX, + int dragOffsetY) { + this.dragOffsetX = dragOffsetX; + this.dragOffsetY = dragOffsetY; + + this.renderer = renderer; + this.tokenBeingResized = tokenBeingResized; + + // theta is the rotation angle clockwise from the positive x-axis to compensate for the +ve + // y-axis pointing downwards in zone space and an unrotated token has facing of -90. + // theta == 0 => token has default rotation. + int theta = -Objects.requireNonNullElse(tokenBeingResized.getFacing(), -90) - 90; + double radians = Math.toRadians(theta); + this.down = new Vector2(-Math.sin(radians), Math.cos(radians)); + this.right = new Vector2(Math.cos(radians), Math.sin(radians)); + + this.originalScaleX = tokenBeingResized.getScaleX(); + this.originalScaleY = tokenBeingResized.getScaleY(); + this.startDragReference = + new ScreenPoint(dragStartX + dragOffsetX, dragStartY + dragOffsetY) + .convertToZone(renderer); + + this.tokenImage = ImageManager.getImage(tokenBeingResized.getImageAssetId()); + } + + public void finish() { + renderer.flush(tokenBeingResized); + MapTool.serverCommand().putToken(renderer.getZone().getId(), tokenBeingResized); + } + + public void dragTo(int mouseX, int mouseY, boolean lockAspectRatio, boolean snapSizeToGrid) { + var currentZp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); + if (snapSizeToGrid) { // snap size to grid + currentZp = getNearestVertex(currentZp); + } else { + // Keep the cursor at the same conceptual position in the drag handle. + currentZp.x += dragOffsetX; + currentZp.y += dragOffsetY; + } + + // Measured in map coordinates + var displacement = Vector2.sub(currentZp, startDragReference); + // Measured in the token's rotated frame, at map scale. + var adjustment = new Vector2(right.dot(displacement), down.dot(displacement)); + + if (lockAspectRatio) { // lock aspect ratio + // In general it is not possible to satisfy both lockAspectRatio and snapSizeToGrid. So + // instead we snap size to grid, then constrain the aspect ratio afterwards, which is this + // logic. + double ratio = tokenImage.getWidth() / (double) tokenImage.getHeight(); + adjustment = new Vector2(adjustment.x, adjustment.x / ratio); + } + + // For snap-to-grid tokens (except background stamps) we anchor at the center of the token. + final var isSnapToGridAndAnchoredAtCenter = + tokenBeingResized.isSnapToGrid() + && tokenBeingResized.getLayer().anchorSnapToGridAtCenter(); + final var snapToGridMultiplier = isSnapToGridAndAnchoredAtCenter ? 2 : 1; + var widthIncrease = adjustment.x * snapToGridMultiplier; + var heightIncrease = adjustment.y * snapToGridMultiplier; + + var originalWidth = tokenImage.getWidth() * originalScaleX; + var originalHeight = tokenImage.getHeight() * originalScaleY; + var updatedWidth = Math.max(1, originalWidth + widthIncrease); + var updatedHeight = Math.max(1, originalHeight + heightIncrease); + + tokenBeingResized.setScaleX(updatedWidth / (double) tokenImage.getWidth()); + tokenBeingResized.setScaleY(updatedHeight / (double) tokenImage.getHeight()); + + renderer.repaint(); + } + + private ZonePoint getNearestVertex(ZonePoint point) { + return renderer.getZone().getNearestVertex(point); + } } } diff --git a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java index b23b3e53d0..2680fca2e0 100644 --- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java @@ -1573,9 +1573,8 @@ public void clearZoneRendererList() { /** Stop the drag of the token, if any is being dragged. */ private void stopTokenDrag() { Tool tool = MapTool.getFrame().getToolbox().getSelectedTool(); - if (tool instanceof PointerTool) { - PointerTool pointer = (PointerTool) tool; - if (pointer.isDraggingToken()) pointer.stopTokenDrag(); + if (tool instanceof PointerTool pointer) { + pointer.stopTokenDrag(); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/DebugRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/DebugRenderer.java index 9d632439b0..36711a7c48 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/DebugRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/DebugRenderer.java @@ -27,7 +27,8 @@ public class DebugRenderer { public DebugRenderer(RenderHelper renderHelper) { this.renderHelper = renderHelper; - palette = new Color[] {Color.red, Color.green, Color.blue}; + palette = + new Color[] {Color.red, Color.green, Color.blue, Color.magenta, Color.orange, Color.yellow}; } public void renderShapes(Graphics2D g2d, Iterable shapes) { diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/SelectionSet.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/SelectionSet.java index 3136684966..3f40659746 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/SelectionSet.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/SelectionSet.java @@ -39,13 +39,12 @@ public class SelectionSet { private final Token token; private Path gridlessPath; - private ZonePoint currentGridlessPoint; - /** Pixel distance (x) from keyToken's origin. */ - int offsetX; + /** The initial location of the key token's drag anchor. */ + private final ZonePoint startPoint; - /** Pixel distance (y) from keyToken's origin. */ - int offsetY; + /** The current location of the key token's drag anchor. */ + private final ZonePoint currentPoint; private RenderPathWorker renderPathTask; private ExecutorService renderPathThreadPool = Executors.newSingleThreadExecutor(); @@ -64,10 +63,14 @@ public SelectionSet( token = renderer.zone.getToken(tokenGUID); + var anchorPoint = token.getDragAnchor(renderer.zone); + + startPoint = new ZonePoint(anchorPoint); + currentPoint = new ZonePoint(anchorPoint); + if (token.isSnapToGrid() && renderer.zone.getGrid().getCapabilities().isSnapToGridSupported()) { if (renderer.zone.getGrid().getCapabilities().isPathingSupported()) { - CellPoint tokenPoint = - renderer.zone.getGrid().convert(new ZonePoint(token.getX(), token.getY())); + CellPoint tokenPoint = renderer.zone.getGrid().convert(currentPoint); walker = renderer.zone.getGrid().createZoneWalker(); walker.setFootprint(token.getFootprint(renderer.zone.getGrid())); @@ -75,18 +78,20 @@ public SelectionSet( } } else { gridlessPath = new Path<>(); - - currentGridlessPoint = new ZonePoint(token.getX(), token.getY()); - gridlessPath.appendWaypoint(currentGridlessPoint); + gridlessPath.appendWaypoint(currentPoint); } } + public ZonePoint getKeyTokenDragAnchorPosition() { + return currentPoint; + } + /** * @return path computation. */ public @Nonnull Path getGridlessPath() { var result = gridlessPath.copy(); - result.appendWaypoint(currentGridlessPoint); + result.appendWaypoint(currentPoint); return result; } @@ -123,13 +128,12 @@ public void renderFinalPath() { } } - public void setOffset(int x, int y) { - offsetX = x; - offsetY = y; + public void update(ZonePoint newAnchorPosition) { + currentPoint.x = newAnchorPosition.x; + currentPoint.y = newAnchorPosition.y; - ZonePoint zp = new ZonePoint(token.getX() + x, token.getY() + y); if (renderer.zone.getGrid().getCapabilities().isPathingSupported() && token.isSnapToGrid()) { - CellPoint point = renderer.zone.getGrid().convert(zp); + CellPoint point = renderer.zone.getGrid().convert(currentPoint); // walker.replaceLastWaypoint(point, restrictMovement); // OLD WAY // New way threaded, off the swing UI thread... @@ -156,9 +160,6 @@ public void setOffset(int x, int y) { token.getTransformedTopology(Zone.TopologyType.MBL), renderer); renderPathThreadPool.execute(renderPathTask); - } else { - currentGridlessPoint.x = zp.x; - currentGridlessPoint.y = zp.y; } } @@ -189,7 +190,7 @@ public ZonePoint getLastWaypoint() { if (cp == null) { // log.info("cellpoint is null! FIXME! You have Walker class updating outside of // thread..."); // Why not save last waypoint to this class? - cp = renderer.zone.getGrid().convert(new ZonePoint(token.getX(), token.getY())); + cp = renderer.zone.getGrid().convert(token.getDragAnchor(renderer.zone)); // log.info("So I set it to: " + cp); } @@ -202,11 +203,11 @@ public ZonePoint getLastWaypoint() { } public int getOffsetX() { - return offsetX; + return currentPoint.x - startPoint.x; } public int getOffsetY() { - return offsetY; + return currentPoint.y - startPoint.y; } public String getPlayerId() { 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 7f409a549e..64ca8bba35 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 @@ -33,6 +33,7 @@ import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.imageio.ImageIO; import javax.swing.*; import net.rptools.lib.CodeTimer; @@ -330,25 +331,29 @@ public void addMoveSelectionSet(String playerId, GUID keyToken, Set tokenL repaintDebouncer.dispatch(); // Jamz: Seems to have no affect? } - public boolean hasMoveSelectionSetMoved(GUID keyToken, ZonePoint point) { + public @Nullable ZonePoint getKeyTokenDragAnchorPosition(GUID keyToken) { + SelectionSet set = selectionSetMap.get(keyToken); + if (set == null) { + return null; + } + return set.getKeyTokenDragAnchorPosition(); + } + + public boolean hasMoveSelectionSetMoved(GUID keyToken, ZonePoint dragAnchorPosition) { SelectionSet set = selectionSetMap.get(keyToken); if (set == null) { return false; } - Token token = zone.getToken(keyToken); - int x = point.x - token.getX(); - int y = point.y - token.getY(); - return set.offsetX != x || set.offsetY != y; + return !set.getKeyTokenDragAnchorPosition().equals(dragAnchorPosition); } - public void updateMoveSelectionSet(GUID keyToken, ZonePoint offset) { + public void updateMoveSelectionSet(GUID keyToken, ZonePoint latestPoint) { SelectionSet set = selectionSetMap.get(keyToken); if (set == null) { return; } - Token token = zone.getToken(keyToken); - set.setOffset(offset.x - token.getX(), offset.y - token.getY()); + set.update(latestPoint); repaintDebouncer.dispatch(); // Jamz: may cause flicker when using AI } @@ -441,14 +446,15 @@ public void commitMoveSelectionSet(GUID keyTokenId) { var tokenPath = path.derive(zone.getGrid(), keyToken, token); token.setLastPath(tokenPath); + // This is the last *anchor* point. var lastPoint = tokenPath.getWayPointList().getLast(); var endPoint = switch (lastPoint) { - case CellPoint cp -> zone.getGrid().convert(cp); + case CellPoint cp -> token.getDragAnchorAsIfLocatedInCell(zone, cp); case ZonePoint zp -> zp; }; - token.setX(endPoint.x); - token.setY(endPoint.y); + token.moveDragAnchorTo(zone, endPoint); + log.info("Token end pos: {}, {}", token.getX(), token.getY()); flush(token); MapTool.serverCommand().putToken(zone.getId(), token); @@ -1141,7 +1147,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) { } timer.stop("lightSourceIconOverlay.paintOverlay"); - debugRenderer.renderShapes(g2d, Arrays.asList(shape, shape2)); + debugRenderer.renderShapes(g2d, Arrays.asList(shape, shape2, shape3, shape4)); } private void delayRendering(ItemRenderer renderer) { @@ -1828,6 +1834,8 @@ public void drawText(String text, int x, int y) { private Shape shape; private Shape shape2; + private Shape shape3; + private Shape shape4; public void setShape(Shape shape) { if (shape == null) { @@ -1835,6 +1843,7 @@ public void setShape(Shape shape) { } this.shape = shape; + this.repaintDebouncer.dispatch(); } public void setShape2(Shape shape) { @@ -1843,6 +1852,25 @@ public void setShape2(Shape shape) { } this.shape2 = shape; + this.repaintDebouncer.dispatch(); + } + + public void setShape3(Shape shape) { + if (shape == null) { + return; + } + + this.shape3 = shape; + this.repaintDebouncer.dispatch(); + } + + public void setShape4(Shape shape) { + if (shape == null) { + return; + } + + this.shape4 = shape; + this.repaintDebouncer.dispatch(); } public void showBlockedMoves( diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java index 59d5a3baca..4595b36f15 100644 --- a/src/main/java/net/rptools/maptool/model/Token.java +++ b/src/main/java/net/rptools/maptool/model/Token.java @@ -1297,6 +1297,12 @@ public void setScaleY(double scaleY) { } /** + * Returns whether the token is constrained to a pre-defined grid size. + * + *

If {@code false}, this implies the token is either natively sized or free sized. If {@code + * true}, the token is sized according to one of the grid's pre-defined sizes, and has a + * meaningful footprint. + * * @return Returns the snapScale. */ public boolean isSnapToScale() { @@ -1572,29 +1578,67 @@ public Rectangle getBounds(Zone zone) { } /** - * Returns the drag offset of the token. + * Return the drag anchor of the token. * - * @param zone the zone where the token is dragged - * @return a point representing the offset + *

The drag anchor is the point relative to which a drag should be applied. For snap-to-grid + * tokens, this will affect which cell they land in. For non-snap-to-grid tokens, this will effect + * where the path line is drawn. + * + * @param zone The zone where the token is being dragged. + * @return The drag anchor of the token. */ - public Point getDragOffset(Zone zone) { + public ZonePoint getDragAnchor(Zone zone) { Grid grid = zone.getGrid(); - int offsetX, offsetY; + int dragAnchorX, dragAnchorY; if (isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) { - if (!getLayer().anchorSnapToGridAtCenter() || isSnapToScale() || getLayer().isTokenLayer()) { + if (!getLayer().isStampLayer() || !getLayer().anchorSnapToGridAtCenter() || isSnapToScale()) { Point2D.Double centerOffset = grid.getCenterOffset(); - offsetX = getX() + (int) centerOffset.x; - offsetY = getY() + (int) centerOffset.y; + dragAnchorX = getX() + (int) centerOffset.x; + dragAnchorY = getY() + (int) centerOffset.y; } else { + // Anchor at the layout center. Rectangle tokenBounds = getBounds(zone); - offsetX = tokenBounds.x + tokenBounds.width / 2; - offsetY = tokenBounds.y + tokenBounds.height / 2; + dragAnchorX = tokenBounds.x + tokenBounds.width / 2 - anchorX; + dragAnchorY = tokenBounds.y + tokenBounds.height / 2 - anchorY; } } else { - offsetX = getX(); - offsetY = getY(); + dragAnchorX = getX() + anchorX; + dragAnchorY = getY() + anchorY; } - return new Point(offsetX, offsetY); + + return new ZonePoint(dragAnchorX, dragAnchorY); + } + + /** + * Updates the token's position so its anchor is located at {@code newDragAnchorPosition}. + * + * @param zone The zone in which the token is moving. + * @param newDragAnchorPosition The new position that the anchor should be located at. + */ + public void moveDragAnchorTo(Zone zone, ZonePoint newDragAnchorPosition) { + var anchor = getDragAnchor(zone); + var offsetX = anchor.x - getX(); + var offsetY = anchor.y - getY(); + + setX(newDragAnchorPosition.x - offsetX); + setY(newDragAnchorPosition.y - offsetY); + } + + /** + * Like {@link #getDragAnchor(Zone)}, but assume the token is in cell {@code cellPoint}. + * + * @param zone The zone that the token lives in. + * @param cellPoint The cell in which the token should pretend to be located. + * @return The drag anchor the token would have if located at {@code cellPoint}. + */ + public ZonePoint getDragAnchorAsIfLocatedInCell(Zone zone, CellPoint cellPoint) { + ZonePoint anchor = getDragAnchor(zone); + ZonePoint nearestGridCellVertex = zone.getGrid().convert(zone.getGrid().convert(anchor)); + ZonePoint targetCellVertex = zone.getGrid().convert(cellPoint); + + return new ZonePoint( + targetCellVertex.x + (anchor.x - nearestGridCellVertex.x), + targetCellVertex.y + (anchor.y - nearestGridCellVertex.y)); } /** diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index 9ec4597f3c..2a52ce3242 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -522,6 +522,8 @@ Preferences.developer.showAiDebugging.label = Enable AI debugging Preferences.developer.showAiDebugging.tooltip = When enabled, adds labels containing the f, g, and h costs calculated by A* during pathfinding, as well as the moves blocked by VBL. Preferences.developer.ignoreGridShapeCache.label = Ignore grid shape cache Preferences.developer.ignoreGridShapeCache.tooltip = When enabled, the grid's shape is recalculated every time it is needed. +Preferences.developer.debugTokenDragging.label = Enable token drag debugging +Preferences.developer.debugTokenDragging.tooltip = When enabled, highlights key points used during token drags, such as anchor points. Preferences.developer.info.developerOptionsInUsePost = If this is not intended, go to {0} > {1} > {2} tab and disable the options there. Preferences.tab.interactions = Interactions Preferences.label.maps.fow = New maps have Fog of War