Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Added Geometry Methods! #160

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
8 changes: 4 additions & 4 deletions src/model/runelite_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ def pick_up_loot(self, items: Union[str, List[str]], supress_warning=True) -> bo
# Locate Ground Items text
if item_text := ocr.find_text(items, self.win.game_view, ocr.PLAIN_11, clr.PURPLE):
for item in item_text:
item.set_rectangle_reference(self.win.game_view)
sorted_by_closest = sorted(item_text, key=Rectangle.distance_from_center)
item.set_parent_rectangle(self.win.game_view)
sorted_by_closest = sorted(item_text, key=Rectangle.distance_from_point)
self.mouse.move_to(sorted_by_closest[0].get_center())
for _ in range(5):
if self.mouseover_text(contains=["Take"] + items, color=[clr.OFF_WHITE, clr.OFF_ORANGE]):
Expand Down Expand Up @@ -197,7 +197,7 @@ def get_nearest_tagged_NPC(self, include_in_combat: bool = False) -> RuneLiteObj
print("No tagged NPCs found.")
return None
for obj in objs:
obj.set_rectangle_reference(self.win.game_view)
obj.set_parent_rectangle(self.win.game_view)
# Sort shapes by distance from player
objs = sorted(objs, key=RuneLiteObject.distance_from_rect_center)
if include_in_combat:
Expand All @@ -220,7 +220,7 @@ def get_all_tagged_in_rect(self, rect: Rectangle, color: clr.Color) -> List[Rune
isolated_colors = clr.isolate_colors(img_rect, color)
objs = rcv.extract_objects(isolated_colors)
for obj in objs:
obj.set_rectangle_reference(rect)
obj.set_parent_rectangle(rect)
return objs

def get_nearest_tag(self, color: clr.Color) -> RuneLiteObject:
Expand Down
240 changes: 223 additions & 17 deletions src/utilities/geometry.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import math
import os
from typing import List, NamedTuple

import cv2
import mss
import numpy as np

if __name__ == "__main__":
import sys

sys.path[0] = os.path.dirname(sys.path[0])

import utilities.random_util as rd
import utilities.debug as debug

Point = NamedTuple("Point", x=int, y=int)

Expand All @@ -22,7 +29,7 @@ class Rectangle:
"""

subtract_list: List[dict] = []
reference_rect = None
parent_rect = None

def __init__(self, left: int, top: int, width: int, height: int):
"""
Expand All @@ -41,14 +48,53 @@ def __init__(self, left: int, top: int, width: int, height: int):
self.width = width
self.height = height

def set_rectangle_reference(self, rect):
def scale(self, scale_width: float = 1, scale_height: float = 1, anchor_x: float = 0.5, anchor_y: float = 0.5):
"""
Scales the rectangle by the given factors for width and height, and adjusts its position based on the anchor point.
Args:
scale_width: The scaling factor for the width of the rectangle (default 1).
scale_height: The scaling factor for the height of the rectangle (default 1).
anchor_x: The horizontal anchor point for scaling (default 0.5, which corresponds to the center).
anchor_y: The vertical anchor point for scaling (default 0.5, which corresponds to the center).
Returns:
The Rectangle object, after scaling.
Examples:
rect = Rectangle(left=10, top=10, width=100, height=100)

# Scale the rectangle by a factor of 2, using the center as the anchor point (default behavior).
rect.scale(2, 2)

# Scale the rectangle by a factor of 2, using the top-left corner as the anchor point.
rect.scale(2, 2, anchor_x=0, anchor_y=0)

# Scale the rectangle by a factor of 2, using the bottom-right corner as the anchor point.
rect.scale(2, 2, anchor_x=1, anchor_y=1)

# Scale the rectangle width by a factor of 1.5 and height by a factor of 2, using the top-right corner as the anchor point.
rect.scale(scale_width=1.5, scale_height=2, anchor_x=1, anchor_y=0)
"""
old_width = self.width
old_height = self.height

new_width = int(self.width * scale_width)
new_height = int(self.height * scale_height)

x_offset = int(old_width * (1 - scale_width) * anchor_x)
y_offset = int(old_height * (1 - scale_height) * anchor_y)

new_left = self.left + x_offset
new_top = self.top + y_offset

return Rectangle(new_left, new_top, new_width, new_height)

def set_parent_rectangle(self, rect):
"""
Sets the rectangle reference of the object.
Args:
rect: A reference to the the rectangle that this object belongs in
(E.g., Bot.win.game_view).
"""
self.reference_rect = rect
self.parent_rect = rect

@classmethod
def from_points(cls, start_point: Point, end_point: Point):
Expand Down Expand Up @@ -110,19 +156,28 @@ def get_center(self) -> Point:
"""
return Point(self.left + self.width // 2, self.top + self.height // 2)

# TODO: Consider changing to this to accept a Point to check against; `distance_from(point: Point)`
def distance_from_center(self) -> Point:
def distance_from_point(self, reference_point: Point = None) -> float:
"""
Gets the distance between the object and it's Rectangle parent center.
Gets the distance between the object and the given reference point.
Useful for sorting lists of Rectangles.
Args:
reference_point: A Point representing the reference point for distance calculation.
Default: The center of the parent rectangle, if available.
Returns:
The distance from the point to the center of the object.
"""
if self.reference_rect is None:
raise ReferenceError("A Rectangle being sorted is missing a reference to the Rectangle it's contained in and therefore cannot be sorted.")
Example:
>>> # Sort based on an arbitrary point
>>> arbitrary_point = Point(100, 200)
>>> sorted_by_arbitrary_point = sorted(some_rectangles, key=lambda rect: rect.distance_from_point(arbitrary_point))
"""
if reference_point is None:
if self.parent_rect is not None:
reference_point = self.parent_rect.get_center()
else:
raise ValueError("A reference point must be provided if there is no parent rectangle.")

center: Point = self.get_center()
rect_center: Point = self.reference_rect.get_center()
return math.dist([center.x, center.y], [rect_center.x, rect_center.y])
return math.dist([center.x, center.y], [reference_point.x, reference_point.y])

def get_top_left(self) -> Point:
"""
Expand All @@ -132,6 +187,22 @@ def get_top_left(self) -> Point:
"""
return Point(self.left, self.top)

def get_center_left(self) -> Point:
"""
Gets the center left point of the rectangle.
Returns:
A Point representing the center left of the rectangle.
"""
return Point(self.left, self.top + self.height // 2)

def get_center_left(self) -> Point:
"""
Gets the center left point of the rectangle.
Returns:
A Point representing the center left of the rectangle.
"""
return Point(self.left, self.top + self.height // 2)

def get_top_right(self) -> Point:
"""
Gets the top right point of the rectangle.
Expand Down Expand Up @@ -194,25 +265,98 @@ def __init__(self, x_min, x_max, y_min, y_max, width, height, center, axis):
self._center = center
self._axis = axis

def set_rectangle_reference(self, rect: Rectangle):
def scale(self, scale_width: float = 1, scale_height: float = 1, anchor_x: float = 0.5, anchor_y: float = 0.5):
"""
Sets the rectangle reference of the object.
Scales the RuneLiteObject by the given factors for width and height, and adjusts its position based on the anchor point.
Args:
scale_width: The scaling factor for the width of the RuneLiteObject (default 1).
scale_height: The scaling factor for the height of the RuneLiteObject (default 1).
anchor_x: The horizontal anchor point for scaling (default 0.5, which corresponds to the center).
anchor_y: The vertical anchor point for scaling (default 0.5, which corresponds to the center).
Returns:
The RuneLiteObject, after scaling.
Examples:
obj = RuneLiteObject(x_min=10, x_max=110, y_min=10, y_max=110, width=100, height=100, center=(60, 60), axis=None)

# Scale the object by a factor of 2, using the center as the anchor point (default behavior).
obj.scale(2, 2)

# Scale the object by a factor of 2, using the top-left corner as the anchor point.
obj.scale(2, 2, anchor_x=0, anchor_y=0)

# Scale the object by a factor of 2, using the bottom-right corner as the anchor point.
obj.scale(2, 2, anchor_x=1, anchor_y=1)

# Scale the object width by a factor of 1.5 and height by a factor of 2, using the top-right corner as the anchor point.
obj.scale(scale_width=1.5, scale_height=2, anchor_x=1, anchor_y=0)
"""
newObject = self
old_width = self._width
old_height = self._height

new_width = int(self._width * scale_width)
new_height = int(self._height * scale_height)

x_offset = int(old_width * (1 - scale_width) * anchor_x)
y_offset = int(old_height * (1 - scale_height) * anchor_y)

new_x_min = self._x_min + x_offset
new_x_max = new_x_min + new_width
new_y_min = self._y_min + y_offset
new_y_max = new_y_min + new_height

new_center = (round((new_x_min + new_x_max) / 2), round((new_y_min + new_y_max) / 2))

# Generate all possible combinations of x and y coordinates inside the bounding box
x_coords = np.arange(new_x_min, new_x_max + 1)
y_coords = np.arange(new_y_min, new_y_max + 1)
xx, yy = np.meshgrid(x_coords, y_coords)
scaled_axis = np.column_stack((xx.ravel(), yy.ravel()))

newObject._x_min = new_x_min
newObject._x_max = new_x_max
newObject._y_min = new_y_min
newObject._y_max = new_y_max
newObject._width = new_width
newObject._height = new_height
newObject._center = new_center
newObject._axis = scaled_axis

return newObject

def set_parent_rectangle(self, rect: Rectangle):
"""
Sets the parent rectangle of the object.
Args:
rect: A reference to the the rectangle that this object belongs in
(E.g., Bot.win.game_view).
"""
self.rect = rect

def center(self) -> Point: # sourcery skip: raise-specific-error
def get_center(self) -> Point:
"""
Gets the center of the object relative to the containing Rectangle.
Gets the center of the object relative to the screen.
Returns:
A Point.
"""
if self.rect is None:
raise ReferenceError("The RuneLiteObject is missing a reference to the Rectangle it's contained in and therefore the center cannot be determined.")
return Point(self._center[0] + self.rect.left, self._center[1] + self.rect.top)

def distance_from_point(self, point: Point) -> float:
"""
Gets the distance between the object and the given point.
Args:
point: A tuple (x, y) representing the coordinates of the point.
Returns:
The distance from the point to the center of the object.
Example:
>>> reference_point = Point(300, 200)
>>> sorted_by_distance = sorted(rl_objects, key=lambda obj: obj.distance_from_point(reference_point))
"""
center: Point = self.get_center()
return math.dist([center.x, center.y], [point.x, point.y])

def distance_from_rect_center(self) -> float:
"""
Gets the distance between the object and it's Rectangle parent center.
Expand All @@ -222,10 +366,49 @@ def distance_from_rect_center(self) -> float:
Note:
Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle.
"""
center: Point = self.center()
center: Point = self.get_center()
rect_center: Point = self.rect.get_center()
return math.dist([center.x, center.y], [rect_center.x, rect_center.y])

def distance_from_rect_left(self) -> float:
"""
Gets the distance between the object and it's Rectangle parent left edge.
Useful for sorting lists of RuneLiteObjects.
Returns:
The distance from the point to the center of the object.
Note:
Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle.
"""
center: Point = self.get_center()
rect_left: Point = self.rect.get_center_left()
return math.dist([center.x, center.y], [rect_left.x, rect_left.y])

def distance_from_top_left(self) -> float:
"""
Gets the distance between the object and it's Rectangle parent top left corner.
Useful for sorting lists of RuneLiteObjects.
Returns:
The distance from the point to the center of the object.
Note:
Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle.
"""
center: Point = self.get_center()
rect_left: Point = self.rect.get_top_left()
return math.dist([center.x, center.y], [rect_left.x, rect_left.y])

def distance_from_top_right(self) -> float:
"""
Gets the distance between the object and it's Rectangle parent top right corner.
Useful for sorting lists of RuneLiteObjects.
Returns:
The distance from the point to the center of the object.
Note:
Only use this if you're sorting a list of RuneLiteObjects that are contained in the same Rectangle.
"""
center: Point = self.get_center()
rect_left: Point = self.rect.get_top_right()
return math.dist([center.x, center.y], [rect_left.x, rect_left.y])

def random_point(self, custom_seeds: List[List[int]] = None) -> Point:
"""
Gets a random point within the object.
Expand All @@ -239,7 +422,7 @@ def random_point(self, custom_seeds: List[List[int]] = None) -> Point:
if custom_seeds is None:
custom_seeds = rd.random_seeds(mod=(self._center[0] + self._center[1]))
x, y = rd.random_point_in(self._x_min, self._y_min, self._width, self._height, custom_seeds)
return self.__relative_point([x, y]) if self.__point_exists([x, y]) else self.center()
return self.__relative_point([x, y]) if self.__point_exists([x, y]) else self.get_center()

def __relative_point(self, point: List[int]) -> Point:
"""
Expand All @@ -258,3 +441,26 @@ def __point_exists(self, p: list) -> bool:
p: The point to check in the format [x, y].
"""
return (self._axis == np.array(p)).all(axis=1).any()


if __name__ == "__main__":
"""
Run this file directly to test this module. You must have an instance of RuneLite open for this to work.
"""
# Get/focus the RuneLite window currently running
win = debug.get_test_window()

# Screenshot the chat box and display it
img = win.chat.screenshot()
cv2.imshow("Chat Box", img)
cv2.waitKey(0)

# Screenshot control panel and display it
img = win.control_panel.screenshot()
cv2.imshow("Control Panel", img)
cv2.waitKey(0)

# Screenshot game view and display it
img = win.game_view.screenshot()
cv2.imshow("Game View", img)
cv2.waitKey(0)