5e Cone Area of Effect on Square Grid: "Template/Stencil" Method
Identify the Bug or Feature request
Relates to but does not fix #1276.
Current cone template does not support D&D5e.
This PR implements a D&D 5e cone AoE on a Square Grid following the "Template" methodology (as opposed to the "Token" methodology).
Description of the Change
Create a new Template type called TriangleTemplate. The goal of this template is to match the cone's area of effect (see bellow quoted from SRD).
Cone
A cone extends in a direction you choose from its point of origin. A cone’s width at a given point along its length is equal to that point’s distance from the point of origin. A cone’s area of effect specifies its maximum length.
A cone’s point of origin is not included in the cone’s area of effect, unless you decide otherwise.
From the perspective of the user, the way the new cone template works is:
- Select the tool
- Click a starting point
- Move the mouse toward the "ending" point (i.e. the center of the cone). See additional notes below.
- Click again to confirm
During step 3, the user can hold control to move the origin like with all other templates. They can also hit control to cancel.
During step 3, the shape of the cone as per the SRD has its boundary drawn (this is to make the resulting area of effect clearer). The cone itself is compared against the grid to determine which grid cells are contained in the area of effect. The grid cells contained in the area of effect have a boundary drawn around them, and are also filled with the color of the drawing tools.
During step 4 the shape of the cone is discarded, and only the resulting area of effect is drawn into the grid.
Here is a screen recording of this feature in action. The jank (slowness and choppyness) is from my screencasting and not how it behaves in the app.
Technical Components
- Add
TriangleTemplateTool.java - Add this tool to the
ToolbarPanel.java - Add
TriangleTemplate.java - Add protobuf definition for
TriangleTemplateDtoindrawing_dto.proto - Add case to
fromDtoinDrawable.java.
TriangleTemplate is the actual area of effect definition (it's parameters, how to draw it, etc.). The parameters are a starting vertex, the direction or theta of the cone and the radius of the cone. The vertex is in pixel space, since this template is not snapped to the grid for the drawing components, only the generated AOE is snapped to the grid. The theta is a real number from [0,360) giving the angle of the cone. The radius is snapped to the size of the grid. It handles painting the AOE onto the grid. The most important method to understand is the paint method. This calculates the co-ordinates of the cone and the gridded bounding rectangle gridSnappedBoundingBox (any grid square that could interesect with the cone is considered). The gridSnappedBoundingBox is added to a PriorityQueue which is used to iterate over candidate regions to establish if they should be in the AoE or not. The process is as follows as long as there is an entry in the Priority Queue:
- If there is no intersection between the candidate AoE and the Cone, we don't include this candidate in the final AoE and do nothing more with this candidate.
- If the candidate AoE is a subset of the Cone (i.e. subtracting the Cone from the candidate leaves us with an empty Area), then the whole candidate is included in the final AoE and we do nothing more with the candidate.
- If neither 1 nor 2 are met, there is some ammount of intersection between the candidate AoE and the Cone. This leaves us with two cases:
a. The Candidate AoE is a single Grid Square: In this case, use the Shoelace formula to determine if more than half of the Grid Square is covered by the Cone. If more than half is covered, include the candidate in the final AoE. The shoelace formula was selected since it is efficient compared with continuing to sub-divide the Grid down to the pixel level and testing the AoE. It also doesn't have any issues with granularity and doesn't require programming any further stopping conditions.
b. If the candidate AoE is not a single grid square, split the candidate AoE in 4 (2x2) and add each of the four resulting squares to the PriorityQueue. Splitting the candidates was chosen since it is more efficient than iterating over each grid square in
gridSnappedBoundingBox, since we can short cicuit some of the AoE processing if we end up in1or2.
TriangleTemplateTool is fairly simple (although long). It is mostly method definitions copied from RadiusTemplateTool. This class handles the drawing state machine (are we drawing, is the CTRL key pressed or not, have we set the starting point, handle final mouse-click, etc.).
Possible Drawbacks
Some stuff I might have overlooked:
- Grid Resizing?
Documentation Notes
Release Notes
WIP Components
-
[x] Decide on toolbar icon.
-
[x] Improve naming in code
-
[x] Make "sensitivity" a parameter of the template so that if this becomes something we want to adjust through the UI we can implement that without changing the core functionality in the future.
-
[x] Set the default sensitivity to 0.
-
[x] Fix bugs with squares being left out.
-
[x] Test performance of a static triangle scaled and rotated using AffineTransform.
-
[ ] Grid adjustment bug. The shape does not change when the grid is changed.
-
[ ] Fix how the name and icon look in the Draw Explorer tab.
-
[x] Cleanup.
-
[ ] Open follow up issues (sensitivity adjustment, "token" method, "gridless" method [1])
[1]
You can also use this method without a grid. If you do so, a creature is included in an area of effect if any part of the miniature's base is overlapped by the template.
This requires interacting with the tokens themselves which is not something templates currently do.
Buuuut... 5e cones are not a right triangle. They are an isoceles triangle with base = height.
I think this is just a bad class name on my part 😆 I implemented what you sent, but goofed on the name. If you look for a call to tan you'll see the constants I have for the angles should match your description.
I can check again later though to be sure.
Any classname suggestions then? IscoceleseTriangleWithBaseEqualsHeight seems a little rough, but idk if something like 5eCone would fly?
On Thu, Dec 28, 2023, at 12:54 AM, Reverend wrote:
Buuuut... 5e cones are not a right triangle. They are an isoceles triangle with base = height. image.png (view on web) https://github.com/RPTools/maptool/assets/45483160/47821e31-be07-4b35-bab7-bbdcff940356
— Reply to this email directly, view it on GitHub https://github.com/RPTools/maptool/pull/4588#issuecomment-1870848603, or unsubscribe https://github.com/notifications/unsubscribe-auth/AON65K7FNYRXF7AXPRZ5GELYLUCQZAVCNFSM6AAAAABBD7T5OGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNZQHA2DQNRQGM. You are receiving this because you authored the thread.Message ID: @.***>
https://github.com/RPTools/maptool/pull/4588/files#diff-d7a136802dcdea693263c74f247c8b1cf45a1527dbb69b3a4b3266cb1822189eR36
// 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);
It seems you use Affine Transforms purely to satisfy overrides and copied code. I wonder if it wouldn't be easier/faster to create a static Path2D once, then use affine rotate and scale to create the drawn triangle.
Path2D aTriangleOfSizeOne = pt(0,0) -> pt1 ->pt2
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
at.rotate(Math.toRadians(rotationAngle));
at.translate(screenPtX, screenPtY);
Shape drawThis = at.createTransformedShape(aTriangleOfSizeOne);
Interesting point... I didn't really understand this component honestly.
Would the resulting code change then be: create static stencil at instantiation of the class then use the radius and theta to scale and rotate as those parameters change from mouse movement? I can also ignore re-renders if those parameters don't change...
Currently I create the stencil from scratch on each mouse movement event and evaluate it against the grid.
On Fri, Dec 29, 2023, at 12:54 AM, Reverend wrote:
It seems you use Affine Transforms purely to satisfy overrides and copied code. I wonder if it wouldn't be easier/faster to create a static Path2D once, then use affine rotate and scale to create the drawn triangle.
— Reply to this email directly, view it on GitHub https://github.com/RPTools/maptool/pull/4588#issuecomment-1871748372, or unsubscribe https://github.com/notifications/unsubscribe-auth/AON65KYR222GPD3UTQ46RPLYLZLIZAVCNFSM6AAAAABBD7T5OGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNZRG42DQMZXGI. You are receiving this because you authored the thread.Message ID: @.***>
In my most recent commit I updated the icons.. Not sure if these actually make sense. I changed the cone template's icon to have a rounded end and basically copied the cone template's old icon to be the icon for the triangle icon.
Would the resulting code change then be: create static stencil at instantiation of the class then use the
radiusandthetato scale and rotate as those parameters change from mouse movement? That was my thinking. Build once, manipulate forever. You aren't creating the world's most complex geometry so I'm not sure if there are efficiency gains or not. Affine transforms essentially do the same manipulations you are already doing to recalculate the point locations. Worth learning about when doing stuff like this.
In https://github.com/RPTools/maptool/pull/4588/commits/9666847129b25e41a8bfdadf8bb526b0ee9aa09d I tested this and the method of re-calculated the cone from scratch each time was faster, but really insignificantly so...
Test results:
320000 Triangles Calculated in Duration Using Transformed Stencil (getConePath): 141us
320000 Triangles Calculated in Duration Using Re-Render from Scratch (getConePathInneficientMethod): 79us
If there is any selection to be made, we should pick whichever has easier to understand logic between getConePath and getConePathInneficientMethod.
Thanks for doing the comparison. Don't think of it as insignificant, think of it as nearly half. Looks like your original approach worked best, probably due to the simplicity of the shape. I shall keep this in mind with my own work using simple shapes.