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
new file mode 100644
index 0000000000..b6e1b5e999
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/tool/drawing/TriangleTemplateTool.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 TriangleTemplateTool 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 TriangleTemplateTool() {}
+
+ /*---------------------------------------------------------------------------------------------
+ * 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 TriangleTemplate();
+ }
+
+ /**
+ * 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);
+ TriangleTemplate t = (TriangleTemplate) 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.triangletemplate.tooltip";
+ }
+
+ /**
+ * @see Tool#getInstructions()
+ */
+ @Override
+ public String getInstructions() {
+ return "tool.triangletemplate.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;
+ TriangleTemplate t = (TriangleTemplate) 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..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,6 +299,9 @@ private OptionPanel createTemplatePanel() {
panel
.add(WallTemplateTool.class)
.setIcon(RessourceManager.getBigIcon(Icons.TOOLBAR_TEMPLATE_WALL));
+ 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 c44e851010..199bd085d2 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
@@ -150,6 +150,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 bf6a15e6c8..3f5db560ec 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
@@ -398,13 +398,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/java/net/rptools/maptool/model/drawing/Drawable.java b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java
index 2b10791b15..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,6 +165,20 @@ static Drawable fromDto(DrawableDto drawableDto) {
drawable.setLayer(Zone.Layer.valueOf(dto.getLayer()));
return drawable;
}
+ case TRIANGLE_TEMPLATE -> {
+ var dto = drawableDto.getTriangleTemplate();
+ var id = GUID.valueOf(dto.getId());
+ var drawable = new TriangleTemplate(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());
+ drawable.setSensitivity(dto.getSensitivity());
+ 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/TriangleTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java
new file mode 100644
index 0000000000..8d5571d340
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/model/drawing/TriangleTemplate.java
@@ -0,0 +1,425 @@
+/*
+ * 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.TriangleTemplateDto;
+
+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
+ // length from the midpoint to the left and right edge
+ // of the base of the cone respectively...
+ 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...
+ 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.
+ }
+
+ public TriangleTemplate(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;
+ }
+
+ // 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) {
+ 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) {
+ 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) {
+ g.fill(aoe);
+ }
+ if (border) {
+ if (this.showAOEOverlay) { // While drawing, it's helpful to see the cone overlay.
+ g.draw(path);
+ }
+ 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);
+ }
+
+ 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
+ // the angle of the cone.
+ double coneSideLength = CONE_SIDE_LENGTH_RATIO * gridSize * radius;
+ double coneAimAngle = theta;
+ 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;
+ }
+
+ 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();
+ }
+
+ 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) {
+ 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 = getBoundingBox(path);
+ Rectangle gridSnappedBoundingBox = getGridSnappedBoundingBox(boundingBox, gridSize);
+ 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 = getSensitivity();
+ 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;
+ }
+
+ @Override
+ public DrawableDto toDto() {
+ var dto = TriangleTemplateDto.newBuilder();
+ dto.setId(getId().toString())
+ .setLayer(getLayer().name())
+ .setZoneId(getZoneId().toString())
+ .setRadius(getRadius())
+ .setVertex(getVertex().toDto())
+ .setTheta(getTheta())
+ .setSensitivity(getSensitivity());
+
+ if (getName() != null) dto.setName(StringValue.of(getName()));
+
+ return DrawableDto.newBuilder().setTriangleTemplate(dto).build();
+ }
+}
diff --git a/src/main/proto/drawing_dto.proto b/src/main/proto/drawing_dto.proto
index 98e45786bd..98b5d93390 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;
+ TriangleTemplateDto triangle_template = 16;
}
}
@@ -119,6 +120,17 @@ message RadiusTemplateDto {
int32 radius = 6;
}
+message TriangleTemplateDto {
+ string id = 1;
+ string layer = 2;
+ google.protobuf.StringValue name = 3;
+ string zoneId = 4;
+ IntPointDto vertex = 5;
+ int32 radius = 6;
+ double theta = 7;
+ double sensitivity = 8;
+}
+
message LineCellTemplateDto {
string id = 1;
string layer = 2;
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 @@
-