From f4f10fc1cfb305c9c76dc7c2a6c6c9f49bedfa5e Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Sat, 1 Jul 2023 12:34:41 -0400 Subject: [PATCH 1/7] Add RightAngleConeTemplate and Tool classes. Correct issue with scaling when drawing shapes. Was drawing in the ZonePoint space (0,0), (0, 1), etc. When I needed to draw in GridPoint space (100,100), (100, 200). Fix cone size and calculate theta instead of endpoint. There is an issue still with calculating the theta on the fly... The issue comes up when we zoom in and out while we are adjusting the angle. Add bounding boxes to the cone. Add logic to reduce cone into AOE squares in grid. Add logic to reduce cone into AOE squares in grid. Add shoelace formula for partially intersecting segments. Improve performance of painting. Instead of iterating over gridsquare, we split the candidate aoe vertically and horizontally and apply tests on the aoe before getting down to the grid size. The tests are more limited when looking at larger than grid size: It's either no intersection with the cone or full intersection... We can only evaluate the overlapping area against the threshold meaningfully at the grid square level. Add persisting of AOE when confirmed. Added dto and related usage in Drawable.java and implement builder in RightAngleConeTemplate.java. Also added some logic to not show the cone overlay when the template is "confirmed" and added to the layer... This is driven by the showOAEOverlay property in RightAngleConeTemplate.java, which is set to true when constructed without an id (i.e. in drawing mode) and false when given an id (i.e. confirmed and persisted into the campaign). I also fixed a minor bug with the behavior of the threshold which was doing the opposite of what is expected ( 90% threshold setting was actually 10%). Remove snapping to grid Use ZonePoint instead of CellPoint to determine radius more fluidly Remove useless comments Re-organize code Run spotlessApply --- .../drawing/RightAngleConeTemplateTool.java | 456 ++++++++++++++++++ .../maptool/client/ui/ToolbarPanel.java | 3 + .../maptool/model/drawing/Drawable.java | 13 + .../model/drawing/RightAngleConeTemplate.java | 350 ++++++++++++++ src/main/proto/drawing_dto.proto | 11 + 5 files changed, 833 insertions(+) create mode 100644 src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java create mode 100644 src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java new file mode 100644 index 0000000000..c8bd0be13e --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java @@ -0,0 +1,456 @@ +/* + * 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.tool.drawing; + +import java.awt.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.geom.AffineTransform; +import javax.swing.*; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ScreenPoint; +import net.rptools.maptool.client.swing.SwingUtil; +import net.rptools.maptool.client.tool.Tool; +import net.rptools.maptool.client.tool.ToolHelper; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.model.Grid; +import net.rptools.maptool.model.ZonePoint; +import net.rptools.maptool.model.drawing.*; + +/** + * Draw a template for a right angle cone with a flat end. This currently only implements cones + * where the base width = height. + * + *

Based on implementation of RadiusTemplateTool, but with different features... + * + *

The template shows the squares that are effected. The player chooses the starting vertex which + * will be the point of the cone, then moves the mouse to set the ending vertex which becomes the + * midpoint of the base of the cone. [Actually implemented in abstract drawing] Holding CTRL while + * moving the mouse allows the user to move the entire template after the initial vertex has been + * set but before the radius has been set. This allows users to move the AOE around to know where + * they are casting a spell. + * + *

This class primarily handles the state machine of: + * + *

ToolSelectedAndInactive PlacingInitialVertex MovingVertexUsingCtrl AdjustingRadiusUsingMouse + * -> Might change this? SendingDrawableToServerForRender... + */ +public class RightAngleConeTemplateTool extends AbstractDrawingTool implements MouseMotionListener { + /*--------------------------------------------------------------------------------------------- + * Instance Variables + *-------------------------------------------------------------------------------------------*/ + + protected AbstractTemplate template = createBaseTemplate(); + + /** This flag controls the painting of the template. */ + protected boolean painting; + + /** + * Has the anchoring point been set? When false, the anchor point is being placed. When true, the + * area of effect is being drawn on the display. + */ + protected boolean anchorSet; + + /** + * The offset used to move the vertex when the control key is pressed. If this value is null + * then this would be the first time that the control key had been reported in the mouse + * event. + */ + protected ZonePoint controlOffset; + + /*--------------------------------------------------------------------------------------------- + * Class Variables + *-------------------------------------------------------------------------------------------*/ + + /** + * The width of the cursor. Since the cursor is a cross, this is the width of the horizontal bar + * and the height of the vertical bar. Always make it an odd number to keep it aligned on the grid + * properly. + */ + public static final int CURSOR_WIDTH = 25; + + /*--------------------------------------------------------------------------------------------- + * Constructor + *-------------------------------------------------------------------------------------------*/ + + public RightAngleConeTemplateTool() {} + + /*--------------------------------------------------------------------------------------------- + * Instance Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * Create the base template for the tool. + * + * @return The right angle cone template that is to be drawn. + */ + protected AbstractTemplate createBaseTemplate() { + return new RightAngleConeTemplate(); + } + + /** + * Calculate the cell at the mouse point. If it is different from the current point, make it the + * current point and repaint. + * + * @param e The event to be checked. + * @param point The current point. + * @return Flag indicating that the value changed. + */ + protected boolean setCellAtMouse(MouseEvent e, ZonePoint point) { + ZonePoint working = getCellAtMouse(e); + if (!working.equals(point)) { + point.x = working.x; + point.y = working.y; + renderer.repaint(); + return true; + } // endif + return false; + } + + /** + * Calculate the cell closest to a mouse point. + * + * @param e The event to be checked. + * @return The cell at the mouse point in screen coordinates. + */ + protected ZonePoint getCellAtMouse(MouseEvent e) { + // Find the cell that the mouse is in. + ZonePoint mouse = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer); + + // Typically here we would snap this to the grid... + // For this template, since we are reducing the shape + // to an aoe, there is no need to start from a specific + // cell, so this will not be snapped to the grid. + // This also helps this template specifically, because + // users will want to make minor adjustments to this template + // and won't want to be stuck on a given corner as a starting + // point. + return mouse; + } + + /** + * Calculate the radius between two cells based on a mouse event. + * + * @param e Mouse event being checked + * @return The radius between the current mouse location and the vertex location. + */ + protected int getRadiusAtMouse(MouseEvent e) { + // To keep the re-sizing a bit smoother, this takes the distance between + // ZonePoints instead of CellPoints. Basically we look at the starting + // mouse and ending mouse point and calculate the distance based on a + // "snapped" distance instead of snapping the starting and ending point + // to the grid and calculating the distance from that. The end result + // should be a much smoother and more predictable template sizing. + ZonePoint workingZonePoint = getCellAtMouse(e); + ZonePoint currentVertex = template.getVertex(); + + int x = Math.abs(workingZonePoint.x - currentVertex.x); + int y = Math.abs(workingZonePoint.y - currentVertex.y); + + int distance = template.getDistance(x, y); + Grid grid = MapTool.getCampaign().getZone(template.getZoneId()).getGrid(); + float gridScaledDistance = (float) distance / (grid.getSize()); + int radius = Math.round(gridScaledDistance); + return radius; + } + + /** + * Paint a cursor + * + * @param g Where to paint. + * @param paint Data to draw the cursor + * @param thickness The thickness of the cursor. + * @param vertex The vertex holding the cursor. + */ + protected void paintCursor(Graphics2D g, Paint paint, float thickness, ZonePoint vertex) { + int halfCursor = CURSOR_WIDTH / 2; + g.setPaint(paint); + g.setStroke(new BasicStroke(thickness)); + g.drawLine(vertex.x - halfCursor, vertex.y, vertex.x + halfCursor, vertex.y); + g.drawLine(vertex.x, vertex.y - halfCursor, vertex.x, vertex.y + halfCursor); + } + + /** + * Get the pen set up to paint the overlay. + * + * @return The pen used to paint the overlay. + */ + protected Pen getPenForOverlay() { + // Get the pen and modify to only show a cursor and the boundary + Pen pen = getPen(); // new copy of pen, OK to modify + pen.setBackgroundMode(Pen.MODE_SOLID); + pen.setForegroundMode(Pen.MODE_SOLID); + pen.setThickness(3); + if (pen.isEraser()) { + pen.setEraser(false); + pen.setPaint(new DrawableColorPaint(Color.WHITE)); + } // endif + return pen; + } + + /** + * Paint the radius value in feet. + * + * @param g Where to paint. + * @param p Vertex where radius is painted. + */ + protected void paintRadius(Graphics2D g, ZonePoint p) { + if (template.getRadius() > 0 && anchorSet) { + ScreenPoint centerText = ScreenPoint.fromZonePoint(renderer, p); + centerText.translate(CURSOR_WIDTH, -CURSOR_WIDTH); + ToolHelper.drawMeasurement( + g, + template.getRadius() * renderer.getZone().getUnitsPerCell(), + (int) centerText.x, + (int) centerText.y); + } // endif + } + + /** + * New instance of the template, at the passed vertex + * + * @param vertex The starting vertex for the new template or null if we should use + * the current template's vertex. + */ + protected void resetTool(ZonePoint vertex) { + anchorSet = false; + if (vertex == null) { + vertex = template.getVertex(); + vertex = new ZonePoint(vertex.x, vertex.y); // Must create copy! + } // endif + template = createBaseTemplate(); + template.setVertex(vertex); + template.setZoneId(renderer.getZone().getId()); + controlOffset = null; + renderer.repaint(); + } + + /** + * Handles setting the vertex when the control key is pressed during mouse movement. A change in + * the passed vertex causes the template to repaint the zone. + * + * @param e The mouse movement event. + * @param vertex The vertex being modified. + */ + protected void handleControlOffset(MouseEvent e, ZonePoint vertex) { + ZonePoint working = getCellAtMouse(e); + if (controlOffset == null) { + controlOffset = working; + controlOffset.x = working.x - vertex.x; + controlOffset.y = working.y - vertex.y; + } else { + working.x = working.x - controlOffset.x; + working.y = working.y - controlOffset.y; + if (!working.equals(vertex)) { + if (vertex == template.getVertex()) { + template.setVertex(working); + } else { + vertex.x = working.x; + vertex.y = working.y; + } // endif + renderer.repaint(); + } // endif + } // endif + } + + /** + * Set the radius on a mouse move after the anchor has been set. + * + * @param e Current mouse locations + */ + protected void setRadiusFromAnchor(MouseEvent e) { + template.setRadius(getRadiusAtMouse(e)); + } + + /** + * Handle mouse movement. Done here so that subclasses can still benefit from the code in + * DefaultTool w/o rewriting it. + * + * @param e Current mouse location + */ + protected void handleMouseMovement(MouseEvent e) { + // Set the anchor + ZonePoint vertex = template.getVertex(); + if (!anchorSet) { + setCellAtMouse(e, vertex); + controlOffset = null; + + // Move the anchor if control pressed. + } else if (SwingUtil.isControlDown(e)) { + handleControlOffset(e, vertex); + + // Set the radius and repaint + } else { + setRadiusFromAnchor(e); + ZonePoint ep = getCellAtMouse(e); + RightAngleConeTemplate t = (RightAngleConeTemplate) template; + t.calculateTheta(e, renderer); + renderer.repaint(); + controlOffset = null; + } // endif + } + + /*--------------------------------------------------------------------------------------------- + * DefaultTool Interface Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * @see net.rptools.maptool.client.tool.DefaultTool#mouseMoved(MouseEvent) + */ + @Override + public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + handleMouseMovement(e); + } + + /*--------------------------------------------------------------------------------------------- + * Overridden AbstractDrawingTool Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * @see net.rptools.maptool.client.ui.zone.ZoneOverlay#paintOverlay(ZoneRenderer, Graphics2D) + */ + @Override + public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { + if (painting && renderer != null) { + Pen pen = getPenForOverlay(); + AffineTransform old = g.getTransform(); + AffineTransform newTransform = g.getTransform(); + newTransform.concatenate(getPaintTransform(renderer)); + g.setTransform(newTransform); + template.draw(g, pen); + Paint paint = pen.getPaint() != null ? pen.getPaint().getPaint() : null; + paintCursor(g, paint, pen.getThickness(), template.getVertex()); + g.setTransform(old); + paintRadius(g, template.getVertex()); + } // endif + } + + /** + * New instance of the template, at the current vertex + * + * @see Tool#resetTool() + */ + @Override + protected void resetTool() { + if (!anchorSet) { + super.resetTool(); + return; + } + resetTool(null); + } + + /** + * It is OK to modify the pen returned by this method + * + * @see AbstractDrawingTool#getPen() + */ + @Override + protected Pen getPen() { + // Just paint the foreground + Pen pen = super.getPen(); + pen.setBackgroundMode(Pen.MODE_SOLID); + return pen; + } + + /** + * @see Tool#detachFrom(ZoneRenderer) + */ + @Override + protected void detachFrom(ZoneRenderer renderer) { + super.detachFrom(renderer); + template.setZoneId(null); + renderer.repaint(); + } + + /** + * @see Tool#attachTo(ZoneRenderer) + */ + @Override + protected void attachTo(ZoneRenderer renderer) { + template.setZoneId(renderer.getZone().getId()); + renderer.repaint(); + super.attachTo(renderer); + } + + /** + * @see Tool#getTooltip() + */ + @Override + public String getTooltip() { + return "tool.radiustemplate.tooltip"; + } + + /** + * @see Tool#getInstructions() + */ + @Override + public String getInstructions() { + return "tool.radiustemplate.instructions"; + } + + /*--------------------------------------------------------------------------------------------- + * MouseListener Interface Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * @see java.awt.event.MouseListener#mousePressed(MouseEvent) + */ + @Override + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + if (!painting) return; + + if (SwingUtilities.isLeftMouseButton(e)) { + controlOffset = null; + if (!anchorSet) { + anchorSet = true; + return; + } // endif + + if (template.getRadius() < AbstractTemplate.MIN_RADIUS) return; + RightAngleConeTemplate t = (RightAngleConeTemplate) template; + + setIsEraser(isEraser(e)); + template.setRadius(getRadiusAtMouse(e)); + + ZonePoint vertex = template.getVertex(); + ZonePoint newPoint = new ZonePoint(vertex.x, vertex.y); + completeDrawable(renderer.getZone().getId(), getPen(), template); + setIsEraser(false); + resetTool(newPoint); + } + } + + /** + * @see java.awt.event.MouseListener#mouseEntered(MouseEvent) + */ + @Override + public void mouseEntered(MouseEvent e) { + super.mouseEntered(e); + painting = true; + renderer.repaint(); + } + + /** + * @see java.awt.event.MouseListener#mouseExited(MouseEvent) + */ + @Override + public void mouseExited(MouseEvent e) { + super.mouseExited(e); + painting = false; + renderer.repaint(); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java index 4916c28680..bd07b73c17 100644 --- a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java @@ -299,6 +299,9 @@ private OptionPanel createTemplatePanel() { panel .add(WallTemplateTool.class) .setIcon(RessourceManager.getBigIcon(Icons.TOOLBAR_TEMPLATE_WALL)); + panel + .add(RightAngleConeTemplateTool.class) + .setIcon(RessourceManager.getBigIcon(Icons.CHAT_SMILEY)); return panel; } diff --git a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java index 2b10791b15..fd7447ac6f 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java @@ -165,6 +165,19 @@ static Drawable fromDto(DrawableDto drawableDto) { drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); return drawable; } + case RIGHT_ANGLE_CONE_TEMPLATE -> { + var dto = drawableDto.getRightAngleConeTemplate(); + var id = GUID.valueOf(dto.getId()); + var drawable = new RightAngleConeTemplate(id); + drawable.setZoneId(GUID.valueOf(dto.getZoneId())); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + if (dto.hasName()) drawable.setName(dto.getName().getValue()); + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + drawable.setTheta(dto.getTheta()); + return drawable; + } case BURST_TEMPLATE -> { var dto = drawableDto.getBurstTemplate(); var id = GUID.valueOf(dto.getId()); diff --git a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java new file mode 100644 index 0000000000..f58f966d30 --- /dev/null +++ b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java @@ -0,0 +1,350 @@ +/* + * 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.model.drawing; + +import com.google.protobuf.StringValue; +import java.awt.*; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.geom.*; +import java.util.Comparator; +import java.util.PriorityQueue; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.model.*; +import net.rptools.maptool.server.proto.drawing.DrawableDto; +import net.rptools.maptool.server.proto.drawing.RightAngleConeTemplateDto; + +public class RightAngleConeTemplate extends AbstractTemplate { + // The definition of the cone is it is as wide as it is + // long, so the cone angle is tan inverse of 1/2, since + // if the length of the cone is 1, there is half it's + // length from the midpoint to the left and right edge + // of the base of the cone respectively... + public static double CONE_ANGLE = Math.atan2(0.5, 1.0); + + // This is the ratio of the cone's side length to the + // length from the point of the cone to the midpoint of the + // base of the cone... + public static double CONE_SIDE_LENGTH_RATIO = 1 / Math.cos(CONE_ANGLE); + + public RightAngleConeTemplate() { + this.showAOEOverlay = true; // While "building" it should show the overlay. + } + + public RightAngleConeTemplate(GUID id) { + super(id); + this.showAOEOverlay = false; + } + + // The angle from the center of the cone to the horizontal. + // This angle lets you adjust where the cone is pointed. + private double theta = 0.0; + + public double getTheta() { + return theta; + } + + public void setTheta(double v) { + theta = v; + } + + private boolean showAOEOverlay = false; + + public void calculateTheta(MouseEvent e, ZoneRenderer renderer) { + if (getRadius() == 0) return; + + // Copy logic for off-setting to match what is in + // ScreenPoint.convertToZone... + // Without this, our theta calculations stops working + // when we zoom or pan the view. + double scale = renderer.getScale(); + double zX = e.getX(); + double zY = e.getY(); + + // Translate + zX -= renderer.getViewOffsetX(); + zY -= renderer.getViewOffsetY(); + + // Scale + zX = Math.floor(zX / scale); + zY = Math.floor(zY / scale); + + ZonePoint sp = getVertex(); + double adjacent = zX - sp.x; + double opposite = zY - sp.y; + setTheta(Math.atan2(opposite, adjacent)); + } + + /*--------------------------------------------------------------------------------------------- + * Overridden AbstractTemplate Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * @see AbstractTemplate#paintBorder(Graphics2D, int, int, int, int, int, int) + */ + @Override + protected void paintBorder( + Graphics2D g, int x, int y, int xOff, int yOff, int gridSize, int distance) { + // NO-OP, not used. I overrode AbstractTemplate#paint in this class and I don't call paintBorder + // and paintArea separately anymore. + } + + @Override + protected void paintArea( + Graphics2D g, int x, int y, int xOff, int yOff, int gridSize, int distance) { + // NO-OP, not used. I overrode AbstractTemplate#paint in this class and I don't call paintBorder + // and paintArea separately anymore. + } + + private void splitAndQueueVertically(Rectangle r, PriorityQueue q, int gridSize) { + // Midpoint is actually "distance to midpoint from x". + // split rectangle horizontally: + int midpoint = r.height / 2; + int midpointSnappedToGrid = midpoint - midpoint % gridSize; + Rectangle top = new Rectangle(r.x, r.y, r.width, midpointSnappedToGrid); + // The bottom split starts at midpoint and has a height of the original height - midpoint + int bottomY = r.y + midpointSnappedToGrid; + int bottomH = r.height - midpointSnappedToGrid; + Rectangle bottom = new Rectangle(r.x, bottomY, r.width, bottomH); + + q.add(top); + q.add(bottom); + } + + private void splitAndQueueHorizontally(Rectangle r, PriorityQueue q, int gridSize) { + // Midpoint is actually "distance to midpoint from x". + // split rectangle horizontally: + int midpoint = r.width / 2; + int midpointSnappedToGrid = midpoint - midpoint % gridSize; + Rectangle left = new Rectangle(r.x, r.y, midpointSnappedToGrid, r.height); + // The right split starts at midpoint and has a width of the original width - midpoint + int rightX = r.x + midpointSnappedToGrid; + int rightW = r.width - midpointSnappedToGrid; + Rectangle right = new Rectangle(rightX, r.y, rightW, r.height); + + if (r.height > gridSize) { + splitAndQueueVertically(left, q, gridSize); + splitAndQueueVertically(right, q, gridSize); + } else { + q.add(left); + q.add(right); + } + } + + @Override + protected void paint(Graphics2D g, boolean border, boolean area) { + Area aoe = this.getArea(); + Path2D.Double path = getConePath(); + + // Paint what is needed. + if (area) { + g.fill(aoe); + } + if (border) { + if (this.showAOEOverlay) { // While drawing, it's helpful to see the cone overlay. + g.draw(path); + } + // g.draw(boundingBox); + // g.draw(gridSnappedBoundingBox); + g.draw(aoe); + } + // endif + } + + /*--------------------------------------------------------------------------------------------- + * Drawable Interface Methods + *-------------------------------------------------------------------------------------------*/ + + /** + * @see Drawable#getBounds() + */ + public Rectangle getBounds() { + if (getZoneId() == null) { + // This avoids a NPE when loading up a campaign + return new Rectangle(); + } + Zone zone = MapTool.getCampaign().getZone(getZoneId()); + if (zone == null) { + return new Rectangle(); + } + int gridSize = zone.getGrid().getSize(); + int quadrantSize = getRadius() * gridSize + BOUNDS_PADDING; + ZonePoint vertex = getVertex(); + return new Rectangle( + vertex.x - quadrantSize, vertex.y - quadrantSize, quadrantSize * 2, quadrantSize * 2); + } + + private Path2D.Double getConePath() { + ZonePoint sp = getVertex(); + + // Only paint if the start and endpoints are not equal and the + // radius is non-zero. + double radius = getRadius(); + if (getRadius() == 0) return new Path2D.Double(); + + Grid grid = MapTool.getCampaign().getZone(getZoneId()).getGrid(); + int gridSize = grid.getSize(); + + // Calculate the position of the vertices of the cone based on + // the angle of the cone. + double coneSideLength = CONE_SIDE_LENGTH_RATIO * gridSize * radius; + double coneAimAngle = getTheta(); + double vertex1X = coneSideLength * Math.cos(coneAimAngle + CONE_ANGLE) + sp.x; + double vertex1Y = coneSideLength * Math.sin(coneAimAngle + CONE_ANGLE) + sp.y; + + double vertex2X = coneSideLength * Math.cos(coneAimAngle - CONE_ANGLE) + sp.x; + double vertex2Y = coneSideLength * Math.sin(coneAimAngle - CONE_ANGLE) + sp.y; + + Path2D.Double path = new Path2D.Double(); + path.moveTo(sp.x, sp.y); + path.lineTo(vertex1X, vertex1Y); + path.lineTo(vertex2X, vertex2Y); + path.lineTo(sp.x, sp.y); + return path; + } + + @Override + public Area getArea() { + if (MapTool.getCampaign().getZone(getZoneId()) == null) { + return new Area(); + } + Grid grid = MapTool.getCampaign().getZone(getZoneId()).getGrid(); + int gridSize = grid.getSize(); + + Path2D.Double path = getConePath(); + + // boundingBox is the minimal bounding box of the cone. + // griddedBoundingBox is the bounding box of all game squares + // that the boundingBox intersects. + Rectangle boundingBox = path.getBounds(); + ZonePoint leftUpper = + new ZonePoint( + gridSize * (int) Math.floor(boundingBox.x / gridSize), + gridSize * (int) Math.floor(boundingBox.y / gridSize)); + + float bottomRightX = boundingBox.x + boundingBox.width; + int bottomRightXSnapped = gridSize * (int) Math.ceil(bottomRightX / gridSize); + float bottomRightY = boundingBox.y + boundingBox.height; + int bottomRightYSnapped = gridSize * (int) Math.ceil(bottomRightY / gridSize); + ZonePoint bottomRight = new ZonePoint(bottomRightXSnapped, bottomRightYSnapped); + Rectangle gridSnappedBoundingBox = + new Rectangle( + leftUpper.x, leftUpper.y, bottomRight.x - leftUpper.x, bottomRight.y - leftUpper.y); + + Area cone = new Area(path); + Area aoe = new Area(); // Empty rectangle that we will update. + /** */ + PriorityQueue queue = + new PriorityQueue( + new Comparator() { + @Override + /** We prioritize smaller rectangles so the Queue stays smaller. */ + public int compare(Rectangle o1, Rectangle o2) { + return o1.height * o1.width - o2.height * o2.width; + } + }); + queue.add(gridSnappedBoundingBox); + while (queue.size() > 0) { + // if fully contained, then add it to the shape + // if empty, + Rectangle candidateRectForAoe = queue.poll(); + // cast to Area to allow subtracting cone. + Area candidateAreaForAoe = new Area(candidateRectForAoe); + candidateAreaForAoe.subtract(cone); + if (candidateAreaForAoe.isEmpty()) { + // If the subtraction leaves the grid square empty, then + // the grid square is fully enclosed by the aoe! + // Add it to the aoe. + aoe.add(new Area(candidateRectForAoe)); + } + // If the gridArea is not equal to the candidateRectForAoe, then + // it means there is some amount of intersection between + // the grid square and the aoe... + else if (!candidateAreaForAoe.equals(new Area(candidateRectForAoe))) { + if (candidateRectForAoe.width > gridSize) { + splitAndQueueHorizontally(candidateRectForAoe, queue, gridSize); + } else if (candidateRectForAoe.height > gridSize) { + splitAndQueueVertically(candidateRectForAoe, queue, gridSize); + } else { + // Otherwise, it's already a gridsquare in size, so let's check + // whether the overlapping area is enough for it to be considered + // in the aoe! + int totalArea = gridSize * gridSize; + // How much of the grid square needs to be covered to be part of aoe. + double requiredPercent = 50; + double thresholdArea = totalArea * (100 - requiredPercent) / 100; + + // Application of the Shoelace formula, using a "flattened" + // path iterator... You can find other examples online and this + // code will look very familiar... but basically + // this is a way of calculating the area under any polygon in o(n) + // where n is the number of sides... + PathIterator it = candidateAreaForAoe.getPathIterator(null, 0.1); + double a = 0.0; + double startingX = 0.0; + double startingY = 0.0; + double previousX = 0.0; + double previousY = 0.0; + double[] coords = new double[6]; + while (!it.isDone()) { + int segmentType = it.currentSegment(coords); + if (segmentType == PathIterator.SEG_MOVETO) { + // set the starting coordinates.. + startingX = coords[0]; + startingY = coords[1]; + } else if (segmentType == PathIterator.SEG_LINETO) { + a += (coords[0] - previousX) * (coords[1] + previousY) / 2.0; + } else if (segmentType == PathIterator.SEG_CLOSE) { + a += (startingX - previousX) * (startingY + previousY) / 2.0; + } + previousX = coords[0]; + previousY = coords[1]; + it.next(); + } + + // Since a is the "subtraction of aoe" from the total area, we're + // actually deciding if the remaining area is less than the threshold + // area... This is all a bit counter-intuitive but could pretty easily + // be reworked. + if (a < thresholdArea) { + aoe.add(new Area(candidateRectForAoe)); + } + } + } // else there is no overlap, so there is nothing to do. + } + + return aoe; + } + + public PathIterator getPathIterator() { + return getArea().getPathIterator(new AffineTransform()); + } + + @Override + public DrawableDto toDto() { + var dto = RightAngleConeTemplateDto.newBuilder(); + dto.setId(getId().toString()) + .setLayer(getLayer().name()) + .setZoneId(getZoneId().toString()) + .setRadius(getRadius()) + .setVertex(getVertex().toDto()) + .setTheta(getTheta()); + + if (getName() != null) dto.setName(StringValue.of(getName())); + + return DrawableDto.newBuilder().setRightAngleConeTemplate(dto).build(); + } +} diff --git a/src/main/proto/drawing_dto.proto b/src/main/proto/drawing_dto.proto index 98e45786bd..5742d409d3 100644 --- a/src/main/proto/drawing_dto.proto +++ b/src/main/proto/drawing_dto.proto @@ -50,6 +50,7 @@ message DrawableDto { BlastTemplateDto blast_template = 13; LineTemplateDto line_template = 14; WallTemplateDto wall_template = 15; + RightAngleConeTemplateDto right_angle_cone_template = 16; } } @@ -119,6 +120,16 @@ message RadiusTemplateDto { int32 radius = 6; } +message RightAngleConeTemplateDto { + string id = 1; + string layer = 2; + google.protobuf.StringValue name = 3; + string zoneId = 4; + IntPointDto vertex = 5; + int32 radius = 6; + double theta = 7; +} + message LineCellTemplateDto { string id = 1; string layer = 2; From 6265048410e6dffb5d7fd0200b8989e916cdb75b Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Fri, 29 Dec 2023 20:50:44 -0500 Subject: [PATCH 2/7] Fix issue with squares being left out --- .../model/drawing/RightAngleConeTemplate.java | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java index f58f966d30..35ee61d942 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java @@ -146,8 +146,22 @@ private void splitAndQueueHorizontally(Rectangle r, PriorityQueue q, @Override protected void paint(Graphics2D g, boolean border, boolean area) { - Area aoe = this.getArea(); + boolean debug = true; Path2D.Double path = getConePath(); + if (debug && border) { + Rectangle boundingBox = this.getBoundingBox(path); + Grid grid = MapTool.getCampaign().getZone(getZoneId()).getGrid(); + int gridSize = grid.getSize(); + Rectangle gridSnappedBoundingBox = this.getGridSnappedBoundingBox(boundingBox, gridSize); + Color normal = g.getColor(); + g.setColor(new Color(0, 0, 255)); + g.draw(boundingBox); + g.setColor(new Color(255, 0, 0)); + g.draw(gridSnappedBoundingBox); + g.setColor(normal); + } + + Area aoe = this.getArea(); // Paint what is needed. if (area) { @@ -157,8 +171,6 @@ protected void paint(Graphics2D g, boolean border, boolean area) { if (this.showAOEOverlay) { // While drawing, it's helpful to see the cone overlay. g.draw(path); } - // g.draw(boundingBox); - // g.draw(gridSnappedBoundingBox); g.draw(aoe); } // endif @@ -216,6 +228,47 @@ private Path2D.Double getConePath() { return path; } + private Rectangle getBoundingBox(Path2D.Double path) { + return path.getBounds(); + } + + private Rectangle getGridSnappedBoundingBox(Rectangle boundingBox, int gridSize) { + // bounding box is not scaled to the grid. + // We want to take the gridSize and snap the bounding box onto the grid. + // In the top left position, we want to snap to the top left corner, + // in the bottom right position we want to snap to the bottom right corner. + double xLeftUpperGridScale = (double) boundingBox.x / gridSize; + double xLeftUpperGridSnapped = Math.floor(xLeftUpperGridScale); + double xLeftUpperZoneScale = gridSize * xLeftUpperGridSnapped; + int xLeftUpper = (int) xLeftUpperZoneScale; + + double yLeftUpperGridScale = (double) boundingBox.y / gridSize; + double yLeftUpperGridSnapped = Math.floor(yLeftUpperGridScale); + double yLeftUpperZoneScale = gridSize * yLeftUpperGridSnapped; + int yLeftUpper = (int) yLeftUpperZoneScale; + + ZonePoint leftUpper = new ZonePoint(xLeftUpper, yLeftUpper); + + float bottomRightX = boundingBox.x + boundingBox.width; + double xRightBottomGridScale = (double) bottomRightX / gridSize; + double xRightBottomGridSnapped = Math.ceil(xRightBottomGridScale); + double xRightBottomZoneScale = gridSize * xRightBottomGridSnapped; + int xRightBottom = (int) xRightBottomZoneScale; + + float bottomRightY = boundingBox.y + boundingBox.height; + double yRightBottomGridScale = (double) bottomRightY / gridSize; + double yRightBottomGridSnapped = Math.ceil(yRightBottomGridScale); + double yRightBottomZoneScale = gridSize * yRightBottomGridSnapped; + int yRightBottom = (int) yRightBottomZoneScale; + + ZonePoint bottomRight = new ZonePoint(xRightBottom, yRightBottom); + Rectangle gridSnappedBoundingBox = + new Rectangle( + leftUpper.x, leftUpper.y, bottomRight.x - leftUpper.x, bottomRight.y - leftUpper.y); + + return gridSnappedBoundingBox; + } + @Override public Area getArea() { if (MapTool.getCampaign().getZone(getZoneId()) == null) { @@ -229,21 +282,8 @@ public Area getArea() { // boundingBox is the minimal bounding box of the cone. // griddedBoundingBox is the bounding box of all game squares // that the boundingBox intersects. - Rectangle boundingBox = path.getBounds(); - ZonePoint leftUpper = - new ZonePoint( - gridSize * (int) Math.floor(boundingBox.x / gridSize), - gridSize * (int) Math.floor(boundingBox.y / gridSize)); - - float bottomRightX = boundingBox.x + boundingBox.width; - int bottomRightXSnapped = gridSize * (int) Math.ceil(bottomRightX / gridSize); - float bottomRightY = boundingBox.y + boundingBox.height; - int bottomRightYSnapped = gridSize * (int) Math.ceil(bottomRightY / gridSize); - ZonePoint bottomRight = new ZonePoint(bottomRightXSnapped, bottomRightYSnapped); - Rectangle gridSnappedBoundingBox = - new Rectangle( - leftUpper.x, leftUpper.y, bottomRight.x - leftUpper.x, bottomRight.y - leftUpper.y); - + Rectangle boundingBox = getBoundingBox(path); + Rectangle gridSnappedBoundingBox = getGridSnappedBoundingBox(boundingBox, gridSize); Area cone = new Area(path); Area aoe = new Area(); // Empty rectangle that we will update. /** */ @@ -284,7 +324,7 @@ else if (!candidateAreaForAoe.equals(new Area(candidateRectForAoe))) { // in the aoe! int totalArea = gridSize * gridSize; // How much of the grid square needs to be covered to be part of aoe. - double requiredPercent = 50; + double requiredPercent = 0; double thresholdArea = totalArea * (100 - requiredPercent) / 100; // Application of the Shoelace formula, using a "flattened" From 0f0749160119172fe59039cf92234b0dd206a22a Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Fri, 29 Dec 2023 20:54:56 -0500 Subject: [PATCH 3/7] Remove unused method getPathIterator --- .../rptools/maptool/model/drawing/RightAngleConeTemplate.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java index 35ee61d942..3fee5010cb 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java @@ -369,10 +369,6 @@ else if (!candidateAreaForAoe.equals(new Area(candidateRectForAoe))) { return aoe; } - public PathIterator getPathIterator() { - return getArea().getPathIterator(new AffineTransform()); - } - @Override public DrawableDto toDto() { var dto = RightAngleConeTemplateDto.newBuilder(); From d2601a985236c52f712d4a47034f03f268de647a Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Sat, 30 Dec 2023 00:40:46 -0500 Subject: [PATCH 4/7] Refactor to TriangleTemplate and add sensitivity parameter --- ...ateTool.java => TriangleTemplateTool.java} | 10 +++---- .../maptool/client/ui/ToolbarPanel.java | 4 +-- .../maptool/model/drawing/Drawable.java | 7 +++-- ...oneTemplate.java => TriangleTemplate.java} | 29 ++++++++++++++----- src/main/proto/drawing_dto.proto | 5 ++-- 5 files changed, 34 insertions(+), 21 deletions(-) rename src/main/java/net/rptools/maptool/client/tool/drawing/{RightAngleConeTemplateTool.java => TriangleTemplateTool.java} (97%) rename src/main/java/net/rptools/maptool/model/drawing/{RightAngleConeTemplate.java => TriangleTemplate.java} (95%) diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java similarity index 97% rename from src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java rename to src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java index c8bd0be13e..32ce8a71ca 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RightAngleConeTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java @@ -47,7 +47,7 @@ *

ToolSelectedAndInactive PlacingInitialVertex MovingVertexUsingCtrl AdjustingRadiusUsingMouse * -> Might change this? SendingDrawableToServerForRender... */ -public class RightAngleConeTemplateTool extends AbstractDrawingTool implements MouseMotionListener { +public class TriangleTemplateTool extends AbstractDrawingTool implements MouseMotionListener { /*--------------------------------------------------------------------------------------------- * Instance Variables *-------------------------------------------------------------------------------------------*/ @@ -85,7 +85,7 @@ public class RightAngleConeTemplateTool extends AbstractDrawingTool implements M * Constructor *-------------------------------------------------------------------------------------------*/ - public RightAngleConeTemplateTool() {} + public TriangleTemplateTool() {} /*--------------------------------------------------------------------------------------------- * Instance Methods @@ -97,7 +97,7 @@ public RightAngleConeTemplateTool() {} * @return The right angle cone template that is to be drawn. */ protected AbstractTemplate createBaseTemplate() { - return new RightAngleConeTemplate(); + return new TriangleTemplate(); } /** @@ -295,7 +295,7 @@ protected void handleMouseMovement(MouseEvent e) { } else { setRadiusFromAnchor(e); ZonePoint ep = getCellAtMouse(e); - RightAngleConeTemplate t = (RightAngleConeTemplate) template; + TriangleTemplate t = (TriangleTemplate) template; t.calculateTheta(e, renderer); renderer.repaint(); controlOffset = null; @@ -421,7 +421,7 @@ public void mousePressed(MouseEvent e) { } // endif if (template.getRadius() < AbstractTemplate.MIN_RADIUS) return; - RightAngleConeTemplate t = (RightAngleConeTemplate) template; + TriangleTemplate t = (TriangleTemplate) template; setIsEraser(isEraser(e)); template.setRadius(getRadiusAtMouse(e)); diff --git a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java index bd07b73c17..f02b819e7e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java @@ -299,9 +299,7 @@ private OptionPanel createTemplatePanel() { panel .add(WallTemplateTool.class) .setIcon(RessourceManager.getBigIcon(Icons.TOOLBAR_TEMPLATE_WALL)); - panel - .add(RightAngleConeTemplateTool.class) - .setIcon(RessourceManager.getBigIcon(Icons.CHAT_SMILEY)); + panel.add(TriangleTemplateTool.class).setIcon(RessourceManager.getBigIcon(Icons.CHAT_SMILEY)); return panel; } diff --git a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java index fd7447ac6f..3c9f91334a 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java @@ -165,10 +165,10 @@ static Drawable fromDto(DrawableDto drawableDto) { drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); return drawable; } - case RIGHT_ANGLE_CONE_TEMPLATE -> { - var dto = drawableDto.getRightAngleConeTemplate(); + case TRIANGLE_TEMPLATE -> { + var dto = drawableDto.getTriangleTemplate(); var id = GUID.valueOf(dto.getId()); - var drawable = new RightAngleConeTemplate(id); + var drawable = new TriangleTemplate(id); drawable.setZoneId(GUID.valueOf(dto.getZoneId())); drawable.setRadius(dto.getRadius()); var vertex = dto.getVertex(); @@ -176,6 +176,7 @@ static Drawable fromDto(DrawableDto drawableDto) { if (dto.hasName()) drawable.setName(dto.getName().getValue()); drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); drawable.setTheta(dto.getTheta()); + drawable.setSensitivity(dto.getSensitivity()); return drawable; } case BURST_TEMPLATE -> { diff --git a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java similarity index 95% rename from src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java rename to src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java index 3fee5010cb..19a00d7582 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/RightAngleConeTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java @@ -25,9 +25,9 @@ import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; import net.rptools.maptool.model.*; import net.rptools.maptool.server.proto.drawing.DrawableDto; -import net.rptools.maptool.server.proto.drawing.RightAngleConeTemplateDto; +import net.rptools.maptool.server.proto.drawing.TriangleTemplateDto; -public class RightAngleConeTemplate extends AbstractTemplate { +public class TriangleTemplate extends AbstractTemplate { // The definition of the cone is it is as wide as it is // long, so the cone angle is tan inverse of 1/2, since // if the length of the cone is 1, there is half it's @@ -40,11 +40,11 @@ public class RightAngleConeTemplate extends AbstractTemplate { // base of the cone... public static double CONE_SIDE_LENGTH_RATIO = 1 / Math.cos(CONE_ANGLE); - public RightAngleConeTemplate() { + public TriangleTemplate() { this.showAOEOverlay = true; // While "building" it should show the overlay. } - public RightAngleConeTemplate(GUID id) { + public TriangleTemplate(GUID id) { super(id); this.showAOEOverlay = false; } @@ -61,6 +61,18 @@ public void setTheta(double v) { theta = v; } + // How much of a grid cell must be covered by the "stencil" + // for the grid cell to be considered part of the AoE. + private double sensitivity = 0.0; + + public double getSensitivity() { + return sensitivity; + } + + public void setSensitivity(double sensitivity) { + this.sensitivity = sensitivity; + } + private boolean showAOEOverlay = false; public void calculateTheta(MouseEvent e, ZoneRenderer renderer) { @@ -324,7 +336,7 @@ else if (!candidateAreaForAoe.equals(new Area(candidateRectForAoe))) { // in the aoe! int totalArea = gridSize * gridSize; // How much of the grid square needs to be covered to be part of aoe. - double requiredPercent = 0; + double requiredPercent = getSensitivity(); double thresholdArea = totalArea * (100 - requiredPercent) / 100; // Application of the Shoelace formula, using a "flattened" @@ -371,16 +383,17 @@ else if (!candidateAreaForAoe.equals(new Area(candidateRectForAoe))) { @Override public DrawableDto toDto() { - var dto = RightAngleConeTemplateDto.newBuilder(); + var dto = TriangleTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()) - .setTheta(getTheta()); + .setTheta(getTheta()) + .setSensitivity(getSensitivity()); if (getName() != null) dto.setName(StringValue.of(getName())); - return DrawableDto.newBuilder().setRightAngleConeTemplate(dto).build(); + return DrawableDto.newBuilder().setTriangleTemplate(dto).build(); } } diff --git a/src/main/proto/drawing_dto.proto b/src/main/proto/drawing_dto.proto index 5742d409d3..98b5d93390 100644 --- a/src/main/proto/drawing_dto.proto +++ b/src/main/proto/drawing_dto.proto @@ -50,7 +50,7 @@ message DrawableDto { BlastTemplateDto blast_template = 13; LineTemplateDto line_template = 14; WallTemplateDto wall_template = 15; - RightAngleConeTemplateDto right_angle_cone_template = 16; + TriangleTemplateDto triangle_template = 16; } } @@ -120,7 +120,7 @@ message RadiusTemplateDto { int32 radius = 6; } -message RightAngleConeTemplateDto { +message TriangleTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; @@ -128,6 +128,7 @@ message RightAngleConeTemplateDto { IntPointDto vertex = 5; int32 radius = 6; double theta = 7; + double sensitivity = 8; } message LineCellTemplateDto { From 17481bcaf7a5217be92390c923463e508b531b44 Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Sat, 30 Dec 2023 01:46:13 -0500 Subject: [PATCH 5/7] Add Triangle Template icon and update Cone Icon. Update tooltips. --- .../tool/drawing/TriangleTemplateTool.java | 4 ++-- .../maptool/client/ui/ToolbarPanel.java | 4 +++- .../maptool/client/ui/theme/Icons.java | 1 + .../client/ui/theme/RessourceManager.java | 5 +++-- .../rod_takehara/ribbon/Cone Template.svg | 4 ++-- .../rod_takehara/ribbon/Triangle Template.svg | 20 +++++++++++++++++++ .../rptools/maptool/language/i18n.properties | 2 ++ 7 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Triangle Template.svg diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java index 32ce8a71ca..b6e1b5e999 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.java @@ -390,7 +390,7 @@ protected void attachTo(ZoneRenderer renderer) { */ @Override public String getTooltip() { - return "tool.radiustemplate.tooltip"; + return "tool.triangletemplate.tooltip"; } /** @@ -398,7 +398,7 @@ public String getTooltip() { */ @Override public String getInstructions() { - return "tool.radiustemplate.instructions"; + return "tool.triangletemplate.instructions"; } /*--------------------------------------------------------------------------------------------- diff --git a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java index f02b819e7e..7cd94b45b6 100644 --- a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java @@ -299,7 +299,9 @@ private OptionPanel createTemplatePanel() { panel .add(WallTemplateTool.class) .setIcon(RessourceManager.getBigIcon(Icons.TOOLBAR_TEMPLATE_WALL)); - panel.add(TriangleTemplateTool.class).setIcon(RessourceManager.getBigIcon(Icons.CHAT_SMILEY)); + panel + .add(TriangleTemplateTool.class) + .setIcon(RessourceManager.getBigIcon(Icons.TOOLBAR_TEMPLATE_TRIANGLE)); return panel; } diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java index f62dd7de38..e37a2b6130 100644 --- a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java +++ b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java @@ -147,6 +147,7 @@ public enum Icons { TOOLBAR_TEMPLATE_RADIUS, TOOLBAR_TEMPLATE_RADIUS_CELL, TOOLBAR_TEMPLATE_WALL, + TOOLBAR_TEMPLATE_TRIANGLE, TOOLBAR_TOKENSELECTION_ALL_OFF, TOOLBAR_TOKENSELECTION_ALL_ON, TOOLBAR_TOKENSELECTION_ME_OFF, diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java index 177cb051cf..567a69932b 100644 --- a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java +++ b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java @@ -368,13 +368,14 @@ public class RessourceManager { put( Icons.TOOLBAR_TEMPLATE_LINE_CELL, ROD_ICONS + "ribbon/Line Template Centered on Grid.svg"); - put(Icons.TOOLBAR_TEMPLATE_OFF, ROD_ICONS + "ribbon/Cone Template.svg"); - put(Icons.TOOLBAR_TEMPLATE_ON, ROD_ICONS + "ribbon/Cone Template.svg"); + put(Icons.TOOLBAR_TEMPLATE_OFF, ROD_ICONS + "ribbon/Triangle Template.svg"); + put(Icons.TOOLBAR_TEMPLATE_ON, ROD_ICONS + "ribbon/Triangle Template.svg"); put(Icons.TOOLBAR_TEMPLATE_RADIUS, ROD_ICONS + "ribbon/Radius Template.svg"); put( Icons.TOOLBAR_TEMPLATE_RADIUS_CELL, ROD_ICONS + "ribbon/Radius Template Centered on Grid.svg"); put(Icons.TOOLBAR_TEMPLATE_WALL, ROD_ICONS + "ribbon/Wall Line Template.svg"); + put(Icons.TOOLBAR_TEMPLATE_TRIANGLE, ROD_ICONS + "ribbon/Triangle Template.svg"); put(Icons.TOOLBAR_TOKENSELECTION_ALL_OFF, ROD_ICONS + "ribbon/All.svg"); put(Icons.TOOLBAR_TOKENSELECTION_ALL_ON, ROD_ICONS + "ribbon/All.svg"); put(Icons.TOOLBAR_TOKENSELECTION_ME_OFF, ROD_ICONS + "ribbon/Me.svg"); diff --git a/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Cone Template.svg b/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Cone Template.svg index cc3dcd3be5..d623a8dcc1 100644 --- a/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Cone Template.svg +++ b/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Cone Template.svg @@ -1,6 +1,6 @@ - + @@ -12,7 +12,7 @@ - + diff --git a/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Triangle Template.svg b/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Triangle Template.svg new file mode 100644 index 0000000000..cc3dcd3be5 --- /dev/null +++ b/src/main/resources/net/rptools/maptool/client/icons/rod_takehara/ribbon/Triangle Template.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index f1704f818c..a1c5d4241e 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -2575,6 +2575,8 @@ tool.polytopo.tooltip = Draw closed polygon VBL. tool.radiusCellTemplate.tooltip = Draw a radius template centered on a grid cell. tool.radiustemplate.instructions = LClick: set initial point, second LClick sets radius; Ctrl: move origin point tool.radiustemplate.tooltip = Draw a radius template. +tool.triangletemplate.instructions = LClick: set initial point, second LClick sets end point; Ctrl: move origin point +tool.triangletemplate.tooltip = Draw a triangle template. tool.rect.instructions = LClick: set initial/final point, Shift+LClick: erase rectangle; Ctrl: snap-to-grid, Alt: origin is centerpoint tool.rect.tooltip = Draw a rectangle. tool.rectexpose.instructions = LClick: set initial/final point to expose; Shift+LClick: hide rectangle; Alt: origin is centerpoint From 9666847129b25e41a8bfdadf8bb526b0ee9aa09d Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Mon, 1 Jan 2024 22:28:40 -0500 Subject: [PATCH 6/7] Add performance testing for AffineTransform change --- .../model/drawing/TriangleTemplate.java | 71 +++++++++++++--- .../TriangleTemplatePerformanceTest.java | 85 +++++++++++++++++++ 2 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java diff --git a/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java index 19a00d7582..8f757a9ebe 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java @@ -33,12 +33,32 @@ public class TriangleTemplate extends AbstractTemplate { // if the length of the cone is 1, there is half it's // length from the midpoint to the left and right edge // of the base of the cone respectively... - public static double CONE_ANGLE = Math.atan2(0.5, 1.0); + private static final double CONE_ANGLE = Math.atan2(0.5, 1.0); // This is the ratio of the cone's side length to the // length from the point of the cone to the midpoint of the // base of the cone... - public static double CONE_SIDE_LENGTH_RATIO = 1 / Math.cos(CONE_ANGLE); + private static final double CONE_SIDE_LENGTH_RATIO = 1 / Math.cos(CONE_ANGLE); + + private static Path2D.Double stencilDefinitionBuilder() { + // Calculate the position of the vertices of the cone based on + // the angle of the cone. + double coneSideLength = CONE_SIDE_LENGTH_RATIO; + double vertex1X = coneSideLength * Math.cos(CONE_ANGLE); + double vertex1Y = coneSideLength * Math.sin(CONE_ANGLE); + + double vertex2X = coneSideLength * Math.cos(-CONE_ANGLE); + double vertex2Y = coneSideLength * Math.sin(-CONE_ANGLE); + + Path2D.Double path = new Path2D.Double(); + path.moveTo(0, 0); + path.lineTo(vertex1X, vertex1Y); + path.lineTo(vertex2X, vertex2Y); + path.lineTo(0, 0); + return path; + } + + private static final Path2D.Double STENCIL_DEFINITION = stencilDefinitionBuilder(); public TriangleTemplate() { this.showAOEOverlay = true; // While "building" it should show the overlay. @@ -211,21 +231,34 @@ public Rectangle getBounds() { vertex.x - quadrantSize, vertex.y - quadrantSize, quadrantSize * 2, quadrantSize * 2); } - private Path2D.Double getConePath() { - ZonePoint sp = getVertex(); - - // Only paint if the start and endpoints are not equal and the - // radius is non-zero. - double radius = getRadius(); - if (getRadius() == 0) return new Path2D.Double(); + protected static Path2D.Double getConePath(ZonePoint sp, int radius, int gridSize, double theta) { + if (radius == 0) return new Path2D.Double(); + double coneScale = gridSize * radius; + + AffineTransform transform = new AffineTransform(); + // Since translate calls AffineTransform.concatenate under the hood, + // we have to un-intuitively (for me at least) call the + // translation before we call the scaling and rotating. + // What this is actually doing to the path below, is + // applying a rotation, then a scaling, then a translation + // from the STENCIL_DEFINITION onto the ZonePoint space, + // using the parameters of the starting point, the angle of the + // triangle and the scale based on the grid size and radius. + transform.translate(sp.x, sp.y); + transform.scale(coneScale, coneScale); + transform.rotate(theta); + + Path2D.Double path = new Path2D.Double(STENCIL_DEFINITION, transform); + return path; + } - Grid grid = MapTool.getCampaign().getZone(getZoneId()).getGrid(); - int gridSize = grid.getSize(); + protected static Path2D.Double getConePathInneficientMethod(ZonePoint sp, int radius, int gridSize, double theta) { + if (radius == 0) return new Path2D.Double(); // Calculate the position of the vertices of the cone based on // the angle of the cone. double coneSideLength = CONE_SIDE_LENGTH_RATIO * gridSize * radius; - double coneAimAngle = getTheta(); + double coneAimAngle = theta; double vertex1X = coneSideLength * Math.cos(coneAimAngle + CONE_ANGLE) + sp.x; double vertex1Y = coneSideLength * Math.sin(coneAimAngle + CONE_ANGLE) + sp.y; @@ -240,6 +273,20 @@ private Path2D.Double getConePath() { return path; } + private Path2D.Double getConePath() { + ZonePoint sp = getVertex(); + + // Only paint if the start and endpoints are not equal and the + // radius is non-zero. + int radius = getRadius(); + Grid grid = MapTool.getCampaign().getZone(getZoneId()).getGrid(); + int gridSize = grid.getSize(); + + double theta = getTheta(); + + return getConePath(sp, radius, gridSize, theta); + } + private Rectangle getBoundingBox(Path2D.Double path) { return path.getBounds(); } diff --git a/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java b/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java new file mode 100644 index 0000000000..b7fe3c8db0 --- /dev/null +++ b/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java @@ -0,0 +1,85 @@ +/* + * 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.model.drawing; + +import net.rptools.maptool.model.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TriangleTemplatePerformanceTest { + @Test + @DisplayName("Test performance of getConePath") + void testRDrawingArea() throws Exception { + ZonePoint[] startPoints = + new ZonePoint[] { + new ZonePoint(100, -50), + new ZonePoint(50, 0), + new ZonePoint(100, 0), + new ZonePoint(150, 0), + new ZonePoint(50, 50), + new ZonePoint(100, 50), + new ZonePoint(150, 50), + new ZonePoint(100, 100), + }; + int thetaSplits = 200; + double [] testThetas = new double[thetaSplits]; + for (int i = 0; i < thetaSplits; i++) { + testThetas[i] = Math.PI / (double) i; + } + + int numberOfRadiuses = 200; + int [] testRadiuses = new int[numberOfRadiuses]; + for (int i = 1; i <= numberOfRadiuses; i++) { + testRadiuses[i-1] = i * 5; + } + + long trianglesCalculated = 0; + long startTime = System.currentTimeMillis(); + for (ZonePoint startPoint : startPoints) { + for (double theta : testThetas) { + for (int radius : testRadiuses) { + Path2D.Double conePath = TriangleTemplate.getConePath(startPoint, radius, 100, theta); + trianglesCalculated++; + assertNotNull(conePath); + } + } + } + long endTime = System.currentTimeMillis(); + System.out.print(trianglesCalculated); + System.out.print(" Triangles Calculated in Duration Using Transformed Stencil: "); + System.out.println(endTime - startTime); + + long trianglesCalculatedInneficient = 0; + long startTimeInefficient = System.currentTimeMillis(); + for (ZonePoint startPoint : startPoints) { + for (double theta : testThetas) { + for (int radius : testRadiuses) { + Path2D.Double conePath = TriangleTemplate.getConePathInneficientMethod(startPoint, radius, 100, theta); + trianglesCalculatedInneficient++; + assertNotNull(conePath); + } + } + } + long endTimeInefficient = System.currentTimeMillis(); + System.out.print(trianglesCalculatedInneficient); + System.out.print(" Triangles Calculated in Duration Using Re-Render from Scratch: "); + System.out.println(endTimeInefficient - startTimeInefficient); + } +} From 64850a662a0f7fd9999d3b4cc75de0543467f88e Mon Sep 17 00:00:00 2001 From: Malcolm Watt Date: Sat, 3 Feb 2024 10:19:50 -0500 Subject: [PATCH 7/7] Select more efficient getConePathMethod --- .../model/drawing/TriangleTemplate.java | 23 +------------------ .../TriangleTemplatePerformanceTest.java | 21 ++--------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java index 8f757a9ebe..8d5571d340 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java @@ -231,28 +231,7 @@ public Rectangle getBounds() { vertex.x - quadrantSize, vertex.y - quadrantSize, quadrantSize * 2, quadrantSize * 2); } - protected static Path2D.Double getConePath(ZonePoint sp, int radius, int gridSize, double theta) { - if (radius == 0) return new Path2D.Double(); - double coneScale = gridSize * radius; - - AffineTransform transform = new AffineTransform(); - // Since translate calls AffineTransform.concatenate under the hood, - // we have to un-intuitively (for me at least) call the - // translation before we call the scaling and rotating. - // What this is actually doing to the path below, is - // applying a rotation, then a scaling, then a translation - // from the STENCIL_DEFINITION onto the ZonePoint space, - // using the parameters of the starting point, the angle of the - // triangle and the scale based on the grid size and radius. - transform.translate(sp.x, sp.y); - transform.scale(coneScale, coneScale); - transform.rotate(theta); - - Path2D.Double path = new Path2D.Double(STENCIL_DEFINITION, transform); - return path; - } - - protected static Path2D.Double getConePathInneficientMethod(ZonePoint sp, int radius, int gridSize, double theta) { + protected static Path2D.Double getConePath(ZonePoint sp, int radius, int gridSize, double theta) { if (radius == 0) return new Path2D.Double(); // Calculate the position of the vertices of the cone based on diff --git a/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java b/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java index b7fe3c8db0..bf8bd44a51 100644 --- a/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java +++ b/src/test/java/net/rptools/maptool/model/drawing/TriangleTemplatePerformanceTest.java @@ -14,14 +14,13 @@ */ package net.rptools.maptool.model.drawing; -import net.rptools.maptool.model.*; +import net.rptools.maptool.model.ZonePoint; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; + import java.awt.geom.Path2D; -import java.awt.geom.Point2D; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class TriangleTemplatePerformanceTest { @Test @@ -65,21 +64,5 @@ void testRDrawingArea() throws Exception { System.out.print(trianglesCalculated); System.out.print(" Triangles Calculated in Duration Using Transformed Stencil: "); System.out.println(endTime - startTime); - - long trianglesCalculatedInneficient = 0; - long startTimeInefficient = System.currentTimeMillis(); - for (ZonePoint startPoint : startPoints) { - for (double theta : testThetas) { - for (int radius : testRadiuses) { - Path2D.Double conePath = TriangleTemplate.getConePathInneficientMethod(startPoint, radius, 100, theta); - trianglesCalculatedInneficient++; - assertNotNull(conePath); - } - } - } - long endTimeInefficient = System.currentTimeMillis(); - System.out.print(trianglesCalculatedInneficient); - System.out.print(" Triangles Calculated in Duration Using Re-Render from Scratch: "); - System.out.println(endTimeInefficient - startTimeInefficient); } }