Skip to content

Commit

Permalink
Improve circle rendering internals
Browse files Browse the repository at this point in the history
Credit to Nopileos2 again for redesigning the internal code that renders target circles
  • Loading branch information
PrincessRTFM authored Jan 24, 2024
2 parents 8e44a8d + de7f7cb commit 24a13f9
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 95 deletions.
178 changes: 84 additions & 94 deletions PositionalGuide/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ namespace PrincessRTFM.PositionalGuide;
public class Plugin: IDalamudPlugin {
public const string Command = "/posguide";

public const double ArcLength = 360 / 16;
public const int ArcSegmentCount = 8;
private const int circleSegmentCount = 128;
// lineIndexToRad and circleSegmentIdxToRad are fixed and only need to be calculated once at the start,
// since neither the number of lines nor the number of circle segments change
private readonly float[] lineIndexToAngle = Enumerable.Range(Configuration.IndexFront, Configuration.IndexFrontLeft + 1).Select(idx => (float)(idx * Math.PI / 4)).ToArray();
private readonly float[] circleSegmentIdxToAngle = Enumerable.Range(0, circleSegmentCount).Select(idx => (float)(idx * (2.0 * Math.PI / circleSegmentCount))).ToArray();
private readonly Vector4[] circleSegmentIdxToColour = new Vector4[circleSegmentCount];

private enum CircleTypes { Target, Outer };

private bool disposed;
Expand Down Expand Up @@ -59,17 +64,19 @@ public Plugin(IDtrBar dtrBar) {
HelpMessage = $"Open {this.Name}'s config window",
ShowInHelp = true,
});

this.dtrEntry = dtrBar.Get(this.Name);
this.setDtrText();
this.dtrEntry.OnClick = this.dtrClickHandler;

Interface.UiBuilder.OpenConfigUi += this.toggleConfigUi;
Interface.UiBuilder.Draw += this.draw;
this.updateCircleColours();
}

private void settingsUpdated() {
this.setDtrText();
this.updateCircleColours();
}

private void dtrClickHandler() {
Expand All @@ -79,6 +86,25 @@ private void dtrClickHandler() {

private void setDtrText() => this.dtrEntry.Text = $"{DTRDisplayName}: {(this.Config.Enabled ? "On" : "Off")}";

private void updateCircleColours() {
// fill a list containing the index and angle of all active lines, for every circle segment look which line is closest to it in terms of angleDifference
// and use the color of that line, only needs to be done once as long as settings stay the same
List<(int index, float angle)> lineIndexesAndAngles = new();
for (int i = Configuration.IndexFront; i <= Configuration.IndexFrontLeft; ++i) {
if (this.Config.DrawGuides[i])
lineIndexesAndAngles.Add((i, this.lineIndexToAngle[i]));
}

if (lineIndexesAndAngles.Count == 0) {
return;
}

for (int i = 0; i < this.circleSegmentIdxToAngle.Length; ++i) {
(int index, float angle) closest = lineIndexesAndAngles.OrderBy(item => angleDifference(this.circleSegmentIdxToAngle[i], item.angle)).First();
this.circleSegmentIdxToColour[i] = this.Config.LineColours[closest.index];
}
}

internal void draw() {
this.windowSystem.Draw();

Expand Down Expand Up @@ -138,45 +164,33 @@ internal void draw() {
if (!targetOnScreen && !limitEither && limitInner)
return;

float arcRadius = target.HitboxRadius;

// this is where we render the lines and the circle segments, which used to be simpler when it was just the lines
for (int arcIndex = 0; arcIndex < 16; ++arcIndex) {
int line = (arcIndex - (arcIndex % 2)) / 2;
bool drawingLine = arcIndex % 2 == 0;

// +X = east, -X = west
// +Z = south, -Z = north
Vector3 guidelineBasePoint = targetPos + new Vector3(0, 0, length);
Vector3 arcBasePoint = targetPos + new Vector3(0, 0, target.HitboxRadius);

// this is basically the same as the old setup for drawing the lines, working on every other iteration of this loop
if (drawingLine && this.Config.DrawGuides[line]) {

// this is the WORLD coordinate for the outer endpoint for the current guideline
Vector3 rotated = rotatePoint(targetPos, guidelineBasePoint, targetFacing + deg2rad(line * 45));
bool endpointOnScreen = Gui.WorldToScreen(rotated, out Vector2 coord);

// depending on the render constraints, we may not actually want to draw anything
if (limitEither) {
if (!targetOnScreen && !endpointOnScreen)
continue;
}
else if (limitOuter && !endpointOnScreen) {
continue;
}

drawing.AddLine(centre, coord, ImGui.GetColorU32(this.Config.LineColours[line]), this.Config.LineThickness);
}

if (this.Config.DrawCircle) {
this.drawCircle(drawing, drawingLine, line, arcIndex, targetPos, arcBasePoint, targetFacing, CircleTypes.Target);
}
// +X = east, -X = west
// +Z = south, -Z = north
Vector3 guidelineBasePoint2 = targetPos + new Vector3(0, 0, length);
Vector3 circleBasePoint = targetPos + new Vector3(0, 0, target.HitboxRadius);
bool anyLineActive = false;
for (int lineIndex = Configuration.IndexFront; lineIndex <= Configuration.IndexFrontLeft; ++lineIndex) {
if (!this.Config.DrawGuides[lineIndex])
continue;
anyLineActive = true;

Vector3 rotated = rotatePoint(targetPos, guidelineBasePoint2, targetFacing + this.lineIndexToAngle[lineIndex]);
bool endpointOnScreen = Gui.WorldToScreen(rotated, out Vector2 coord);
if (limitEither) {
if (!targetOnScreen && !endpointOnScreen)
continue;
} else if (limitOuter && !endpointOnScreen) {
continue;
}
drawing.AddLine(centre, coord, ImGui.GetColorU32(this.Config.LineColours[lineIndex]), this.Config.LineThickness);
}

if (this.Config.DrawOuterCircle) {
this.drawCircle(drawing, drawingLine, line, arcIndex, targetPos, arcBasePoint, targetFacing, CircleTypes.Outer);
if (this.Config.DrawCircle) {
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Target);
}

}
if (this.Config.DrawOuterCircle) {
this.drawCircle(drawing, targetPos, circleBasePoint, targetFacing, anyLineActive, CircleTypes.Outer);

}

Expand Down Expand Up @@ -235,68 +249,44 @@ internal void draw() {
ImGui.End();
}

private void drawCircle(ImDrawListPtr drawing, bool drawingLine, int line, int arcIndex, Vector3 targetPos, Vector3 basePoint, float targetFacing, CircleTypes circleType) {
Vector4 arcColour = new Vector4(1, 0, 0, 1);
Vector3 arcBasePoint = basePoint;
private void drawCircle(ImDrawListPtr drawing, Vector3 targetPos, Vector3 basePoint, float targetFacing, bool anyLineActive, CircleTypes circleType) {
Vector4 circleColour = new Vector4(1, 0, 0, 1);
Vector3 circleBasePoint = basePoint;
bool forceCircleColour = false;

// handle all this differences between circles here to not blow up the argument list even further
switch (circleType) {
case CircleTypes.Target:
arcColour = this.Config.LineColours[Configuration.IndexCircle];
forceCircleColour = this.Config.AlwaysUseCircleColours || this.Config.AlwaysUseCircleColoursTarget;
circleColour = this.Config.LineColours[Configuration.IndexCircle];
forceCircleColour = this.Config.AlwaysUseCircleColours || this.Config.AlwaysUseCircleColoursTarget || !anyLineActive;
break;
case CircleTypes.Outer:
arcColour = this.Config.LineColours[Configuration.IndexOuterCircle];
forceCircleColour = this.Config.AlwaysUseCircleColours || this.Config.AlwaysUseCircleColoursOuter;
arcBasePoint += new Vector3(0, 0, this.Config.SoftOuterCircleRange);
circleColour = this.Config.LineColours[Configuration.IndexOuterCircle];
forceCircleColour = this.Config.AlwaysUseCircleColours || this.Config.AlwaysUseCircleColoursOuter || !anyLineActive;
circleBasePoint += new Vector3(0, 0, this.Config.SoftOuterCircleRange);
break;
}

if (!forceCircleColour) {
// we basically bounce back and forth, starting at the arc's adjacent line and then going left (negative) when the arc is on the left or right (positive) otherwise,
// then flipping repeatedly until we either find an enabled line to pull the colour from, or cover all of the lines and find nothing
int sign = drawingLine ? 1 : -1; // inverted from the above description because the first loop uses 0, so this is flipped once before it has any effect
int arcColourLine = line;
for (int offset = 0; offset < Configuration.IndexCircle; ++offset) {
arcColourLine += offset * sign;
sign *= -1;
if (arcColourLine >= Configuration.IndexCircle)
arcColourLine %= Configuration.IndexCircle;
else if (arcColourLine < 0)
arcColourLine = Configuration.IndexCircle + arcColourLine; // add because it's negative
if (this.Config.DrawGuides[arcColourLine]) {
arcColour = this.Config.LineColours[arcColourLine];
break;
}
}
Vector3 startPoint = rotatePoint(targetPos, circleBasePoint, targetFacing);
Vector3[] points = circlePoints(targetPos, startPoint, this.circleSegmentIdxToAngle).ToArray();

(Vector2 point, bool render)[] screenPoints = new (Vector2 point, bool render)[points.Length];
for (int i = 0; i < points.Length; ++i) {
bool render = Gui.WorldToScreen(points[i], out Vector2 screenPoint);
screenPoints[i] = (screenPoint, render);
}

// unfortunately, ImGui doesn't actually offer a way to draw arcs, so we're gonna have to do it manually... by drawing a series of line segments
// which means we need to calculate the endpoints for those line segments, along the arc, based on the calculated endpoints of the arc
// we start with the endpoints of the arc itself here
double endpointOffsetLeft = ((arcIndex - 1) * ArcLength) + (arcIndex / 2d);
double endpointOffsetRight = (arcIndex * ArcLength) + (arcIndex / 2d);
double totalArcRadians = deg2rad(endpointOffsetRight - endpointOffsetLeft);
Vector3 arcEndpointLeft = rotatePoint(targetPos, arcBasePoint, targetFacing + deg2rad(endpointOffsetLeft));
Vector3 arcEndpointRight = rotatePoint(targetPos, arcBasePoint, targetFacing + deg2rad(endpointOffsetRight));

// only render the arc segments if the entire arc is on screen
bool renderingArc = true;
renderingArc &= Gui.WorldToScreen(arcEndpointLeft, out Vector2 screenEndpointLeft);
renderingArc &= Gui.WorldToScreen(arcEndpointRight, out Vector2 screenEndpointRight);

if (renderingArc) {
Vector2[] points = arcPoints(targetPos, arcEndpointLeft, totalArcRadians, ArcSegmentCount + 1)
.Select(world => { Gui.WorldToScreen(world, out Vector2 screen); return screen; })
.ToArray();
for (int i = 1; i < points.Length; ++i)
drawing.AddLine(points[i - 1], points[i], ImGui.GetColorU32(arcColour), this.Config.LineThickness + 2);
for (int i = 0; i < screenPoints.Length; ++i) {
int nextIndex = (i + 1) % screenPoints.Length;
(Vector2 point, bool render) screenPoint1 = screenPoints[i];
(Vector2 point, bool render) screenPoint2 = screenPoints[nextIndex];

if (screenPoint1.render && screenPoint2.render) {
Vector4 colour = forceCircleColour ? circleColour : this.circleSegmentIdxToColour[i];
drawing.AddLine(screenPoint1.point, screenPoint2.point, ImGui.GetColorU32(colour), this.Config.LineThickness + 2);
}
}
}

private static double deg2rad(double degrees) => degrees * Math.PI / 180;

private static Vector2 rotatePoint(Vector2 centre, Vector2 originalPoint, double angleRadians) {
// Adapted (read: shamelessly stolen) from https://github.com/PunishedPineapple/Distance

Expand All @@ -316,14 +306,14 @@ private static Vector3 rotatePoint(Vector3 centre, Vector3 originalPoint, double

private static double angleBetween(Vector2 vertex, Vector2 a, Vector2 b) => Math.Atan2(b.Y - vertex.Y, b.X - vertex.X) - Math.Atan2(a.Y - vertex.Y, a.X - vertex.X);
private static double angleBetween(Vector3 vertex, Vector3 a, Vector3 b) => angleBetween(new Vector2(vertex.X, vertex.Z), new Vector2(a.X, a.Z), new Vector2(b.X, b.Z));
private static float angleDifference(float a, float b) => (float)(Math.Min(Math.Abs(a - b), Math.Abs(Math.Abs(a - b) - (2 * Math.PI))));

private static IEnumerable<Vector2> arcPoints(Vector2 centre, Vector2 start, double totalAngle, int count) {
double angleStep = totalAngle / (count - 1);
for (int i = 0; i < count; i++)
yield return rotatePoint(centre, start, angleStep * i);
private static IEnumerable<Vector2> circlePoints(Vector2 centre, Vector2 start, float[] angles) {
foreach (float angle in angles)
yield return rotatePoint(centre, start, angle);
}
private static IEnumerable<Vector3> arcPoints(Vector3 centre, Vector3 start, double totalAngle, int count)
=> arcPoints(new Vector2(centre.X, centre.Z), new Vector2(start.X, start.Z), totalAngle, count).Select(v2 => new Vector3(v2.X, centre.Y, v2.Y));
private static IEnumerable<Vector3> circlePoints(Vector3 centre, Vector3 start, float[] angles)
=> circlePoints(new Vector2(centre.X, centre.Z), new Vector2(start.X, start.Z), angles).Select(v2 => new Vector3(v2.X, centre.Y, v2.Y));

internal void onPluginCommand(string command, string arguments) {
string[] args = arguments.Trim().Split();
Expand Down
2 changes: 1 addition & 1 deletion PositionalGuide/PositionalGuide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Product>PositionalGuide</Product>
<Version>4.3.0</Version>
<Version>4.3.1</Version>
<Description>Provides drawn guidelines to see where you need to stand to hit enemies with positionals</Description>
<Copyright>Copyleft VariableVixen 2022</Copyright>
<PackageProjectUrl>https://github.com/PrincessRTFM/PositionalAssistant</PackageProjectUrl>
Expand Down

0 comments on commit 24a13f9

Please sign in to comment.