Skip to content

resources: add clockwise point sorting helpers#1077

Open
Bing-su wants to merge 8 commits into
OverlayPlugin:mainfrom
Bing-su:feat/sorts
Open

resources: add clockwise point sorting helpers#1077
Bing-su wants to merge 8 commits into
OverlayPlugin:mainfrom
Bing-su:feat/sorts

Conversation

@Bing-su

@Bing-su Bing-su commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

I am opening this PR because, while thinking about a Korean server strategy implementation for Forsaken, I thought these functions would be useful as shared utilities.

These are two functions that take objects containing x and y coordinates and return them sorted in clockwise order.

sortPointsClockwise

This function sorts target points clockwise around their center point when the points are clustered on one side.

If the target points are on a semicircle, it returns the sorted points; otherwise, it returns undefined. (There may be cases where the points appear sortable even when they span more than a semicircle, but those are very limited exceptions.)

img
img

sortPointsClockwise with reference point

This function sorts points clockwise from the position of a specific reference point.

Because there is a reference point, there are no constraints on the positions of the points.

img

2026-06-13

  • Merged sortPointsClockwiseFrom into sortPointsClockwise via an optional { reference } option.
  • Removed the semicircle restriction so sortPointsClockwise now always returns T[].

@github-actions github-actions Bot added resources /resources test /test, /ui/test needs-review Awaiting review labels Jun 10, 2026
@xiashtra xiashtra requested a review from valarnin June 14, 2026 11:04
@valarnin

Copy link
Copy Markdown
Collaborator

I really like this in concept, but the code here seems to be vastly over complicated and a bit narrow in scope at the same time, if that makes sense.

I think something like the following (only minimally tested) would work much better, and includes sorting for both an array of direction outputs and for points.

type AnyDirection = DirectionOutputCardinal | DirectionOutputIntercard | DirectionOutput8 | DirectionOutput16;

// Example usage:
// const dirs: DirectionOutputCardinal[] = ['dirN', 'dirW'];
// dirs.sort(getSortDirectionsClockwiseFunction('dirE'));
// `dirs` should equal `['dirW', 'dirN']`
const getSortDirectionsClockwiseFunction = (from: AnyDirection | undefined) => {
  // Default to dirN
  let offset = 0;
  if (from !== undefined)
    offset = output16Dir.indexOf(from);

  const count = output16Dir.length;

  return (left: AnyDirection, right: AnyDirection) => {
    const rightIndex = (count + (output16Dir.indexOf(right) - offset)) % count;
    const leftIndex = (count + (output16Dir.indexOf(left) - offset)) % count;
    return rightIndex - leftIndex;
  };
};

type Point = {
  x: number;
  y: number;
};

const xyToHeading = (x: number, y: number, centerX: number, centerY: number) => {
  x = x - centerX;
  y = y - centerY;
  return Math.atan2(x, y);
};

// Example usage:
// getSortPointsClockwiseFunction
// const points = [{ x: 101, y: 101 }, { x: 99, y: 99 }];
// points.sort(getSortPointsClockwiseFunction({x: 100, y: 100}, {x: 99, y: 101}));
// `points` should now equal `[{ x: 99, y: 99 }, { x: 101, y: 101 }]`
const getSortPointsClockwiseFunction = <T extends Point>(center: T, reference: number | T = 0) => {
  // Convert point to heading if needed
  const offset = typeof reference === 'object' ? xyToHeading(reference.x, reference.y, center.x, center.y) : reference;

  const twoPI = Math.PI * 2;

  return (left: T, right: T) => {
    // Get our base headings for the two points
    const rightHeading = xyToHeading(right.x, right.y, center.x, center.y);
    const leftHeading = xyToHeading(left.x, left.y, center.x, center.y);

    // Adjust by reference offset
    const rightHeadingOffset = (twoPI + (rightHeading - offset)) % twoPI;
    const leftHeadingOffset = (twoPI + (leftHeading - offset)) % twoPI;

    return rightHeadingOffset - leftHeadingOffset;
  };
};

@Bing-su

Bing-su commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you for the review.

I also felt that my code was somewhat verbose, but it was intended to handle a boundary case.
When reference is set to 0, it calculated as south. As a result, the following situation can occur.

wantnot1

To solve this, I used an approach where the first point clockwise from the largest gap is used as the starting point.

An alternative would be to require callers to always calculate and pass in reference, but I do not think that would be an easy approach.

@valarnin

Copy link
Copy Markdown
Collaborator

When reference is set to 0, it calculated as south. As a result, the following situation can occur.

Sorry, the default reference in my example was meant to be north, so it should have been Math.PI.


I feel that any sort of automatic attempt to determine the "correct" reference angle is doomed to failure due to edge cases. If any point in the array is controlled by the player, it becomes functionally impossible to account for all edge cases. As such, that logic should not be a generic use case, and instead it should live in the trigger set file it's used by.

If I were implementing this, I would write a generic sort function like my example which can be exposed in Directions, and then in the trigger set I would determine the reference point by calculating the average angle of the points and then inverting it (adding Math.PI). Because such logic lives in the trigger set, it is capable of using only known-good data to determine the reference angle.

@Bing-su

Bing-su commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

@valarnin I've rewritten the implementation, so please review again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review Awaiting review resources /resources test /test, /ui/test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants