Compare commits
No commits in common. "main" and "0.3" have entirely different histories.
@ -5,11 +5,8 @@ build-backend = 'setuptools.build_meta'
|
||||
[tool.setuptools-git-versioning]
|
||||
enabled = true
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
dependencies = {file = ["requirements.txt"]}
|
||||
|
||||
[project]
|
||||
dynamic = ['version', 'dependencies']
|
||||
dynamic = ['version']
|
||||
name = "shs-tools"
|
||||
authors = [
|
||||
{ name="Stefan Harmuth", email="pennywise@drock.de" },
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
bs4~=0.0.1
|
||||
beautifulsoup4~=4.12
|
||||
requests~=2.32
|
||||
tqdm~=4.66
|
||||
beautifulsoup4==4.11.1
|
||||
fishhook~=0.2.5
|
||||
pygame~=2.4.0
|
||||
requests==2.28.1
|
||||
setuptools==65.6.3
|
||||
|
||||
131
src/tools/aoc.py
131
src/tools/aoc.py
@ -1,17 +1,14 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
import webbrowser
|
||||
from bs4 import BeautifulSoup
|
||||
from .datafiles import JSONFile
|
||||
from .stopwatch import StopWatch
|
||||
from typing import Any, Callable, Type
|
||||
from tqdm.auto import tqdm
|
||||
from typing import Any, Callable, List, Tuple, Type
|
||||
from .tools import get_script_dir
|
||||
|
||||
BASE_PATH = get_script_dir()
|
||||
@ -21,9 +18,9 @@ INPUTS_PATH = os.path.join(BASE_PATH, "inputs")
|
||||
class AOCDay:
|
||||
year: int
|
||||
day: int
|
||||
input: list[str] # our input is always a list of str/lines
|
||||
inputs: list[list[tuple[Any, str]]]
|
||||
part_func: list[Callable]
|
||||
input: List[str] # our input is always a list of str/lines
|
||||
inputs: List[List[Tuple[Any, str]]]
|
||||
part_func: List[Callable]
|
||||
|
||||
def __init__(self, year: int, day: int):
|
||||
self.day = day
|
||||
@ -31,11 +28,6 @@ class AOCDay:
|
||||
self.part_func = [self.part1, self.part2]
|
||||
self._current_test_file = None
|
||||
self._current_test_solution = None
|
||||
self.DP = {}
|
||||
self.SEEN = set()
|
||||
self.__main_progress_bar_id = None
|
||||
self.__main_progress_bar_pos = 0
|
||||
self.progress_bars = {}
|
||||
|
||||
def part1(self) -> Any:
|
||||
raise NotImplementedError()
|
||||
@ -43,16 +35,6 @@ class AOCDay:
|
||||
def part2(self) -> Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_test(self) -> bool:
|
||||
return "test" in self._current_test_file
|
||||
|
||||
def _call_part_func(self, func: Callable) -> Any:
|
||||
ans = func()
|
||||
for p, pbar in self.progress_bars.items():
|
||||
pbar.close()
|
||||
self.progress_bars = {}
|
||||
return ans
|
||||
|
||||
def run_part(
|
||||
self,
|
||||
part: int,
|
||||
@ -68,27 +50,25 @@ class AOCDay:
|
||||
self._load_input(input_file)
|
||||
|
||||
if not measure_runtime or case_count < len(self.inputs[part]) - 1:
|
||||
self.DP = {}
|
||||
self.seen_reset()
|
||||
answer = self._call_part_func(self.part_func[part])
|
||||
answer = self.part_func[part]()
|
||||
else:
|
||||
stopwatch = StopWatch(auto_start=False)
|
||||
self.__main_progress_bar_pos = 1
|
||||
for _ in tqdm(range(timeit_number), desc=f"Part {part+1}", leave=False):
|
||||
self.DP = {}
|
||||
self.seen_reset()
|
||||
stopwatch.start()
|
||||
answer = self._call_part_func(self.part_func[part])
|
||||
stopwatch = StopWatch()
|
||||
for _ in range(timeit_number):
|
||||
answer = self.part_func[part]()
|
||||
stopwatch.stop()
|
||||
exec_time = stopwatch.avg_string(timeit_number)
|
||||
|
||||
if solution is None:
|
||||
print_solution(self.day, part + 1, answer, solution, case_count, exec_time)
|
||||
print_solution(
|
||||
self.day, part + 1, answer, solution, case_count, exec_time
|
||||
)
|
||||
if answer not in {"", b"", None, b"None", "None", 0, "0"}:
|
||||
self._submit(part + 1, answer)
|
||||
else:
|
||||
if verbose or answer != solution:
|
||||
print_solution(self.day, part + 1, answer, solution, case_count, exec_time)
|
||||
print_solution(
|
||||
self.day, part + 1, answer, solution, case_count, exec_time
|
||||
)
|
||||
|
||||
if answer != solution:
|
||||
return False
|
||||
@ -110,9 +90,6 @@ class AOCDay:
|
||||
self.run_part(1, verbose, measure_runtime, timeit_number)
|
||||
|
||||
def _load_input(self, filename):
|
||||
if not os.path.exists(INPUTS_PATH):
|
||||
os.mkdir(INPUTS_PATH)
|
||||
|
||||
file_path = os.path.join(INPUTS_PATH, filename)
|
||||
if not os.path.exists(file_path):
|
||||
self._download_input(file_path)
|
||||
@ -128,7 +105,10 @@ class AOCDay:
|
||||
cookies={"session": session_id},
|
||||
)
|
||||
if not response.ok:
|
||||
print("FAILED to download input: (%s) %s" % (response.status_code, response.text))
|
||||
print(
|
||||
"FAILED to download input: (%s) %s"
|
||||
% (response.status_code, response.text)
|
||||
)
|
||||
return
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
@ -171,19 +151,22 @@ class AOCDay:
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
print("Failed to submit answer: (%s) %s" % (response.status_code, response.text))
|
||||
print(
|
||||
"Failed to submit answer: (%s) %s"
|
||||
% (response.status_code, response.text)
|
||||
)
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
message = soup.article.text
|
||||
if "That's the right answer" in message:
|
||||
answer_cache[str_day][str_part]["correct"] = answer
|
||||
has_rank = re.findall(r"You achieved.*rank (\d+)", message)
|
||||
print("That's correct!%s" % f" (Rank {has_rank[0]})" if has_rank else "")
|
||||
webbrowser.open("https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day))
|
||||
print("That's correct!")
|
||||
webbrowser.open(
|
||||
"https://adventofcode.com/%d/day/%d#part2" % (self.year, self.day)
|
||||
)
|
||||
elif "That's not the right answer" in message:
|
||||
hilo = re.findall("your answer is too (high|low)", message)
|
||||
answer_cache[str_day][str_part]["wrong"].append(answer)
|
||||
print("That's WRONG%s!" % (f" (too {hilo[0]})" if hilo else ""))
|
||||
print("That's WRONG!")
|
||||
elif "You gave an answer too recently" in message:
|
||||
# WAIT and retry
|
||||
wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait"
|
||||
@ -221,13 +204,9 @@ class AOCDay:
|
||||
else:
|
||||
return self.input.copy()
|
||||
|
||||
def getIntsFromInput(self) -> list:
|
||||
if len(self.input) == 1:
|
||||
return list(map(int, re.findall(r"-?\d+", self.input[0])))
|
||||
else:
|
||||
return [list(map(int, re.findall(r"-?\d+", l))) for l in self.input]
|
||||
|
||||
def getMultiLineInputAsArray(self, return_type: Type = None, join_char: str = None) -> list[list[Any]]:
|
||||
def getMultiLineInputAsArray(
|
||||
self, return_type: Type = None, join_char: str = None
|
||||
) -> List:
|
||||
"""
|
||||
get input for day x as 2d array, split by empty lines
|
||||
"""
|
||||
@ -253,50 +232,28 @@ class AOCDay:
|
||||
return return_array
|
||||
|
||||
def getInputAsArraySplit(
|
||||
self, split_char: str = ",", return_type: Type | list[Type] = None
|
||||
) -> list[Any] | list[list[Any]]:
|
||||
self, split_char: str = ",", return_type: Type | List[Type] = None
|
||||
) -> List:
|
||||
"""
|
||||
get input for day x with the lines split by split_char
|
||||
if input has only one line, returns a 1d array with the values
|
||||
if input has multiple lines, returns a 2d array (a[line][values])
|
||||
"""
|
||||
if len(self.input) == 1:
|
||||
return split_line(line=self.input[0], split_char=split_char, return_type=return_type)
|
||||
return split_line(
|
||||
line=self.input[0], split_char=split_char, return_type=return_type
|
||||
)
|
||||
else:
|
||||
return_array = []
|
||||
for line in self.input:
|
||||
return_array.append(split_line(line=line, split_char=split_char, return_type=return_type))
|
||||
return_array.append(
|
||||
split_line(
|
||||
line=line, split_char=split_char, return_type=return_type
|
||||
)
|
||||
)
|
||||
|
||||
return return_array
|
||||
|
||||
def progress(self, total: int, add: int = 1, bar_id: str = None) -> None:
|
||||
if bar_id is None:
|
||||
if self.__main_progress_bar_id is None:
|
||||
self.__main_progress_bar_id = uuid.uuid4()
|
||||
bar_id = self.__main_progress_bar_id
|
||||
|
||||
if bar_id not in self.progress_bars:
|
||||
pbar = tqdm(
|
||||
total=total,
|
||||
position=len(self.progress_bars) + self.__main_progress_bar_pos,
|
||||
leave=False,
|
||||
file=sys.stdout,
|
||||
)
|
||||
self.progress_bars[bar_id] = pbar
|
||||
|
||||
pbar = self.progress_bars[bar_id]
|
||||
pbar.update(add)
|
||||
|
||||
def seen_reset(self):
|
||||
self.SEEN = set()
|
||||
|
||||
def seen(self, value: Any, auto_add: bool = True) -> bool:
|
||||
if value in self.SEEN:
|
||||
return True
|
||||
if auto_add:
|
||||
self.SEEN.add(value)
|
||||
return False
|
||||
|
||||
|
||||
def print_solution(
|
||||
day: int,
|
||||
@ -332,13 +289,15 @@ def print_solution(
|
||||
print("Day %s, Part %s - Average run time: %s" % (day, part, exec_time))
|
||||
|
||||
|
||||
def split_line(line, split_char: str = ",", return_type: Type | list[Type] = None):
|
||||
def split_line(line, split_char: str = ",", return_type: Type | List[Type] = None):
|
||||
if split_char:
|
||||
line = line.split(split_char)
|
||||
|
||||
if return_type is None:
|
||||
return line
|
||||
elif isinstance(return_type, list):
|
||||
return [return_type[x](i) if len(return_type) > x else i for x, i in enumerate(line)]
|
||||
return [
|
||||
return_type[x](i) if len(return_type) > x else i for x, i in enumerate(line)
|
||||
]
|
||||
else:
|
||||
return [return_type(i) for i in line]
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
from math import log10
|
||||
|
||||
ABSOLUTE_ZERO_C = -273.15
|
||||
|
||||
|
||||
def celsius_to_fahrenheit(celsius: float) -> float:
|
||||
return celsius * 9 / 5 + 32
|
||||
|
||||
|
||||
def fahrenheit_to_celsius(fahrenheit: float) -> float:
|
||||
return (fahrenheit - 32) / 9 * 5
|
||||
|
||||
|
||||
def celsius_to_kelvin(celsius: float) -> float:
|
||||
return celsius - ABSOLUTE_ZERO_C
|
||||
|
||||
|
||||
def kelvin_to_celsius(kelvin: float) -> float:
|
||||
return kelvin + ABSOLUTE_ZERO_C
|
||||
|
||||
|
||||
def fahrenheit_to_kelvin(fahrenheit: float) -> float:
|
||||
return celsius_to_kelvin(fahrenheit_to_celsius(fahrenheit))
|
||||
|
||||
|
||||
def kelvin_to_fahrenheit(kelvin: float) -> float:
|
||||
return celsius_to_fahrenheit(kelvin_to_celsius(kelvin))
|
||||
|
||||
|
||||
def saturation_vapor_pressure(temperature: float) -> float:
|
||||
"""
|
||||
Saturation vapor pressure for a given temperature (in °C) in hPa
|
||||
"""
|
||||
a = 7.5 if temperature >= 0 else 7.6
|
||||
b = 237.3 if temperature >= 0 else 240.7
|
||||
return 6.1078 * 10 ** ((a * temperature) / (b + temperature))
|
||||
|
||||
|
||||
def vapor_pressure(relative_humidity: float, temperature: float) -> float:
|
||||
"""
|
||||
Vapor pressure for a given relative humidity (in %) and temperature (in °C) in hPa
|
||||
"""
|
||||
return relative_humidity / 100 * saturation_vapor_pressure(temperature)
|
||||
|
||||
|
||||
def relative_humidity(temperature: float, due_point: float) -> float:
|
||||
"""
|
||||
Relative humidity for a given temperature (in °C) in and due point (in °C)
|
||||
"""
|
||||
return 100 * saturation_vapor_pressure(due_point) / saturation_vapor_pressure(temperature)
|
||||
|
||||
|
||||
def absolute_humidity_from_humidity(relative_humidity: float, temperature: float) -> float:
|
||||
"""
|
||||
Absolute humidity for a given relative humidity (in %) and temperature (in °C) in g/m³
|
||||
"""
|
||||
mw = 18.016
|
||||
rs = 8314.3
|
||||
return 10**5 * mw / rs * vapor_pressure(relative_humidity, temperature) / celsius_to_kelvin(temperature)
|
||||
|
||||
|
||||
def absolute_humidity_from_due_point(due_point: float, temperature: float) -> float:
|
||||
"""
|
||||
Absolute humidity for a given due point (in °C) and temperature (in °C) in g/m³
|
||||
"""
|
||||
mw = 18.016
|
||||
rs = 8314.3
|
||||
return 10**5 * mw / rs * saturation_vapor_pressure(due_point) / celsius_to_kelvin(temperature)
|
||||
|
||||
|
||||
def due_point(relative_humidity: float, temperature: float) -> float:
|
||||
"""
|
||||
Due Point for a given relative humidity (in %) and temperature (in °C) in °C
|
||||
"""
|
||||
a = 7.5 if temperature >= 0 else 7.6
|
||||
b = 237.3 if temperature >= 0 else 240.7
|
||||
v = log10(vapor_pressure(relative_humidity, temperature) / 6.1078)
|
||||
|
||||
return b * v / (a - v)
|
||||
@ -1,24 +1,8 @@
|
||||
from __future__ import annotations
|
||||
from enum import Enum
|
||||
from math import gcd, sqrt, inf, atan2, degrees, isclose
|
||||
from math import gcd, sqrt, inf, atan2, degrees
|
||||
from .math import round_half_up
|
||||
from typing import Union, List, Iterable
|
||||
from .tools import minmax
|
||||
|
||||
|
||||
NEIGHBOURS = [(0, -1), (1, 0), (0, 1), (-1, 0)]
|
||||
NEIGHBOURS_3D = [(0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 0, -1), (0, 1, 0), (-1, 0, 0)]
|
||||
DIAGONAL_NEIGHBOURS = [(-1, -1), (1, -1), (-1, 1), (1, 1)]
|
||||
DIAGONAL_NEIGHBOURS_3D = [
|
||||
(-1, -1, -1),
|
||||
(1, -1, -1),
|
||||
(-1, -1, 1),
|
||||
(1, -1, 1),
|
||||
(-1, 1, -1),
|
||||
(-1, 1, 1),
|
||||
(1, 1, -1),
|
||||
(1, 1, 1),
|
||||
]
|
||||
from typing import Union, List
|
||||
|
||||
|
||||
class DistanceAlgorithm(Enum):
|
||||
@ -30,30 +14,30 @@ class DistanceAlgorithm(Enum):
|
||||
|
||||
|
||||
class Coordinate(tuple):
|
||||
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None):
|
||||
return tuple.__new__(cls, (x, y, z))
|
||||
def __new__(cls, x: int, y: int, z: int = None) -> Coordinate:
|
||||
return tuple.__new__(Coordinate, (x, y, z))
|
||||
|
||||
@property
|
||||
def x(self) -> int | float:
|
||||
def x(self):
|
||||
return self[0]
|
||||
|
||||
@property
|
||||
def y(self) -> int | float:
|
||||
def y(self):
|
||||
return self[1]
|
||||
|
||||
@property
|
||||
def z(self) -> int | float:
|
||||
def z(self):
|
||||
return self[2]
|
||||
|
||||
def is3D(self) -> bool:
|
||||
return self[2] is not None
|
||||
return self.z is not None
|
||||
|
||||
def getDistanceTo(
|
||||
self,
|
||||
target: Coordinate | tuple,
|
||||
target: Coordinate,
|
||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
||||
includeDiagonals: bool = False,
|
||||
) -> int | float:
|
||||
) -> Union[int, float]:
|
||||
"""
|
||||
Get distance to target Coordinate
|
||||
|
||||
@ -64,34 +48,40 @@ class Coordinate(tuple):
|
||||
:return: Distance to Target
|
||||
"""
|
||||
if algorithm == DistanceAlgorithm.EUCLIDEAN:
|
||||
if self[2] is None:
|
||||
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2)
|
||||
if self.z is None:
|
||||
return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2)
|
||||
else:
|
||||
return sqrt(
|
||||
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2 + abs(self[2] - target[2]) ** 2
|
||||
abs(self.x - target.x) ** 2
|
||||
+ abs(self.y - target.y) ** 2
|
||||
+ abs(self.z - target.z) ** 2
|
||||
)
|
||||
elif algorithm == DistanceAlgorithm.CHEBYSHEV:
|
||||
if self[2] is None:
|
||||
return max(abs(target[0] - self[0]), abs(target[1] - self[1]))
|
||||
if self.z is None:
|
||||
return max(abs(target.x - self.x), abs(target.y - self.y))
|
||||
else:
|
||||
return max(
|
||||
abs(target[0] - self[0]),
|
||||
abs(target[1] - self[1]),
|
||||
abs(target[2] - self[2]),
|
||||
abs(target.x - self.x),
|
||||
abs(target.y - self.y),
|
||||
abs(target.z - self.z),
|
||||
)
|
||||
elif algorithm == DistanceAlgorithm.MANHATTAN:
|
||||
if not includeDiagonals:
|
||||
if self[2] is None:
|
||||
return abs(self[0] - target[0]) + abs(self[1] - target[1])
|
||||
if self.z is None:
|
||||
return abs(self.x - target.x) + abs(self.y - target.y)
|
||||
else:
|
||||
return abs(self[0] - target[0]) + abs(self[1] - target[1]) + abs(self[2] - target[2])
|
||||
return (
|
||||
abs(self.x - target.x)
|
||||
+ abs(self.y - target.y)
|
||||
+ abs(self.z - target.z)
|
||||
)
|
||||
else:
|
||||
dist = [abs(self[0] - target[0]), abs(self[1] - target[1])]
|
||||
if self[2] is None:
|
||||
dist = [abs(self.x - target.x), abs(self.y - target.y)]
|
||||
if self.z is None:
|
||||
o_dist = max(dist) - min(dist)
|
||||
return o_dist + 1.4 * min(dist)
|
||||
else:
|
||||
dist.append(abs(self[2] - target[2]))
|
||||
dist.append(abs(self.z - target.z))
|
||||
d_steps = min(dist)
|
||||
dist.remove(min(dist))
|
||||
dist = [x - d_steps for x in dist]
|
||||
@ -100,48 +90,60 @@ class Coordinate(tuple):
|
||||
|
||||
def inBoundaries(
|
||||
self,
|
||||
minX: int | float,
|
||||
minY: int | float,
|
||||
maxX: int | float,
|
||||
maxY: int | float,
|
||||
minZ: int | float = -inf,
|
||||
maxZ: int | float = inf,
|
||||
minX: int,
|
||||
minY: int,
|
||||
maxX: int,
|
||||
maxY: int,
|
||||
minZ: int = -inf,
|
||||
maxZ: int = inf,
|
||||
) -> bool:
|
||||
if self[2] is None:
|
||||
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
|
||||
if self.z is None:
|
||||
return minX <= self.x <= maxX and minY <= self.y <= maxY
|
||||
else:
|
||||
return minX <= self[0] <= maxX and minY <= self[1] <= maxY and minZ <= self[2] <= maxZ
|
||||
return (
|
||||
minX <= self.x <= maxX
|
||||
and minY <= self.y <= maxY
|
||||
and minZ <= self.z <= maxZ
|
||||
)
|
||||
|
||||
def getCircle(
|
||||
self,
|
||||
radius: int | float = 1,
|
||||
radius: int = 1,
|
||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
||||
minX: int | float = -inf,
|
||||
minY: int | float = -inf,
|
||||
maxX: int | float = inf,
|
||||
maxY: int | float = inf,
|
||||
minZ: int | float = -inf,
|
||||
maxZ: int | float = inf,
|
||||
minX: int = -inf,
|
||||
minY: int = -inf,
|
||||
maxX: int = inf,
|
||||
maxY: int = inf,
|
||||
minZ: int = -inf,
|
||||
maxZ: int = inf,
|
||||
) -> list[Coordinate]:
|
||||
ret = []
|
||||
if self[2] is None: # mode 2D
|
||||
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
|
||||
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
|
||||
if self.z is None: # mode 2D
|
||||
for x in range(self.x - radius * 2, self.x + radius * 2 + 1):
|
||||
for y in range(self.y - radius * 2, self.y + radius * 2 + 1):
|
||||
target = Coordinate(x, y)
|
||||
if not target.inBoundaries(minX, minY, maxX, maxY):
|
||||
continue
|
||||
dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False))
|
||||
dist = round_half_up(
|
||||
self.getDistanceTo(
|
||||
target, algorithm=algorithm, includeDiagonals=False
|
||||
)
|
||||
)
|
||||
if dist == radius:
|
||||
ret.append(target)
|
||||
|
||||
else:
|
||||
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
|
||||
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
|
||||
for z in range(self[2] - radius * 2, self[2] + radius * 2 + 1):
|
||||
for x in range(self.x - radius * 2, self.x + radius * 2 + 1):
|
||||
for y in range(self.y - radius * 2, self.y + radius * 2 + 1):
|
||||
for z in range(self.z - radius * 2, self.z + radius * 2 + 1):
|
||||
target = Coordinate(x, y)
|
||||
if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ):
|
||||
continue
|
||||
dist = round_half_up(self.getDistanceTo(target, algorithm=algorithm, includeDiagonals=False))
|
||||
dist = round_half_up(
|
||||
self.getDistanceTo(
|
||||
target, algorithm=algorithm, includeDiagonals=False
|
||||
)
|
||||
)
|
||||
if dist == radius:
|
||||
ret.append(target)
|
||||
|
||||
@ -150,13 +152,12 @@ class Coordinate(tuple):
|
||||
def getNeighbours(
|
||||
self,
|
||||
includeDiagonal: bool = True,
|
||||
minX: int | float = -inf,
|
||||
minY: int | float = -inf,
|
||||
maxX: int | float = inf,
|
||||
maxY: int | float = inf,
|
||||
minZ: int | float = -inf,
|
||||
maxZ: int | float = inf,
|
||||
dist: int | float = 1,
|
||||
minX: int = -inf,
|
||||
minY: int = -inf,
|
||||
maxX: int = inf,
|
||||
maxY: int = inf,
|
||||
minZ: int = -inf,
|
||||
maxZ: int = inf,
|
||||
) -> list[Coordinate]:
|
||||
"""
|
||||
Get a list of neighbouring coordinates.
|
||||
@ -168,33 +169,60 @@ class Coordinate(tuple):
|
||||
:param maxX: ignore all neighbours that would have an X value above this
|
||||
:param maxY: ignore all neighbours that would have an Y value above this
|
||||
:param maxZ: ignore all neighbours that would have an Z value above this
|
||||
:param dist: distance to neighbour coordinates
|
||||
:return: list of Coordinate
|
||||
"""
|
||||
if self[2] is None:
|
||||
nb_list = [x * dist for x in NEIGHBOURS]
|
||||
if self.z is None:
|
||||
if includeDiagonal:
|
||||
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS]
|
||||
nb_list = [
|
||||
(-1, -1),
|
||||
(-1, 0),
|
||||
(-1, 1),
|
||||
(0, -1),
|
||||
(0, 1),
|
||||
(1, -1),
|
||||
(1, 0),
|
||||
(1, 1),
|
||||
]
|
||||
else:
|
||||
nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
||||
|
||||
for dx, dy in nb_list:
|
||||
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
|
||||
yield self.__class__(self[0] + dx, self[1] + dy)
|
||||
if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY:
|
||||
yield self.__class__(self.x + dx, self.y + dy)
|
||||
else:
|
||||
nb_list = [x * dist for x in NEIGHBOURS_3D]
|
||||
if includeDiagonal:
|
||||
nb_list += [x * dist for x in DIAGONAL_NEIGHBOURS_3D]
|
||||
nb_list = [
|
||||
(x, y, z)
|
||||
for x in [-1, 0, 1]
|
||||
for y in [-1, 0, 1]
|
||||
for z in [-1, 0, 1]
|
||||
]
|
||||
nb_list.remove((0, 0, 0))
|
||||
else:
|
||||
nb_list = [
|
||||
(-1, 0, 0),
|
||||
(0, -1, 0),
|
||||
(1, 0, 0),
|
||||
(0, 1, 0),
|
||||
(0, 0, 1),
|
||||
(0, 0, -1),
|
||||
]
|
||||
|
||||
for dx, dy, dz in nb_list:
|
||||
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY and minZ <= self[2] + dz <= maxZ:
|
||||
yield self.__class__(self[0] + dx, self[1] + dy, self[2] + dz)
|
||||
if (
|
||||
minX <= self.x + dx <= maxX
|
||||
and minY <= self.y + dy <= maxY
|
||||
and minZ <= self.z + dz <= maxZ
|
||||
):
|
||||
yield self.__class__(self.x + dx, self.y + dy, self.z + dz)
|
||||
|
||||
def getAngleTo(self, target: Coordinate | tuple, normalized: bool = False) -> float:
|
||||
def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float:
|
||||
"""normalized returns an angle going clockwise with 0 starting in the 'north'"""
|
||||
if self[2] is not None:
|
||||
if self.z is not None:
|
||||
raise NotImplementedError() # which angle?!?!
|
||||
|
||||
dx = target[0] - self[0]
|
||||
dy = target[1] - self[1]
|
||||
dx = target.x - self.x
|
||||
dy = target.y - self.y
|
||||
if not normalized:
|
||||
return degrees(atan2(dy, dx))
|
||||
else:
|
||||
@ -204,117 +232,212 @@ class Coordinate(tuple):
|
||||
else:
|
||||
return 180.0 + abs(angle)
|
||||
|
||||
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
|
||||
"""this will probably not yield what you expect, when using float coordinates"""
|
||||
if target == self:
|
||||
return [self]
|
||||
def getLineTo(self, target: Coordinate) -> List[Coordinate]:
|
||||
diff = target - self
|
||||
|
||||
if self[2] is None:
|
||||
steps = gcd(diff[0], diff[1])
|
||||
step_x = diff[0] // steps
|
||||
step_y = diff[1] // steps
|
||||
return [self.__class__(self[0] + step_x * i, self[1] + step_y * i) for i in range(steps + 1)]
|
||||
else:
|
||||
steps = gcd(diff[0], diff[1], diff[2])
|
||||
step_x = diff[0] // steps
|
||||
step_y = diff[1] // steps
|
||||
step_z = diff[2] // steps
|
||||
if self.z is None:
|
||||
steps = gcd(diff.x, diff.y)
|
||||
step_x = diff.x // steps
|
||||
step_y = diff.y // steps
|
||||
return [
|
||||
self.__class__(self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i)
|
||||
self.__class__(self.x + step_x * i, self.y + step_y * i)
|
||||
for i in range(steps + 1)
|
||||
]
|
||||
else:
|
||||
steps = gcd(diff.x, diff.y, diff.z)
|
||||
step_x = diff.x // steps
|
||||
step_y = diff.y // steps
|
||||
step_z = diff.z // steps
|
||||
return [
|
||||
self.__class__(
|
||||
self.x + step_x * i, self.y + step_y * i, self.z + step_z * i
|
||||
)
|
||||
for i in range(steps + 1)
|
||||
]
|
||||
|
||||
def reverse(self) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(-self[0], -self[1])
|
||||
if self.z is None:
|
||||
return self.__class__(-self.x, -self.y)
|
||||
else:
|
||||
return self.__class__(-self[0], -self[1], -self[2])
|
||||
return self.__class__(-self.x, -self.y, -self.z)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self[0], self[1], self[2]))
|
||||
|
||||
def __eq__(self, other: Coordinate | tuple) -> bool:
|
||||
if self[2] is None:
|
||||
return self[0] == other[0] and self[1] == other[1]
|
||||
def __add__(self, other: Coordinate) -> Coordinate:
|
||||
if self.z is None:
|
||||
return self.__class__(self.x + other.x, self.y + other.y)
|
||||
else:
|
||||
return self[0] == other[0] and self[1] == other[1] and self[2] == other[2]
|
||||
return self.__class__(self.x + other.x, self.y + other.y, self.z + other.z)
|
||||
|
||||
def __add__(self, other: Coordinate | tuple) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] + other[0], self[1] + other[1])
|
||||
def __sub__(self, other: Coordinate) -> Coordinate:
|
||||
if self.z is None:
|
||||
return self.__class__(self.x - other.x, self.y - other.y)
|
||||
else:
|
||||
return self.__class__(self[0] + other[0], self[1] + other[1], self[2] + other[2])
|
||||
return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z)
|
||||
|
||||
def __sub__(self, other: Coordinate | tuple) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] - other[0], self[1] - other[1])
|
||||
def __mul__(self, other: int) -> Coordinate:
|
||||
if self.z is None:
|
||||
return self.__class__(self.x * other, self.y * other)
|
||||
else:
|
||||
return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2])
|
||||
return self.__class__(self.x * other, self.y * other, self.z * other)
|
||||
|
||||
def __mul__(self, other: int | float) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] * other, self[1] * other)
|
||||
def __floordiv__(self, other) -> Coordinate:
|
||||
if self.z is None:
|
||||
return self.__class__(self.x // other, self.y // other)
|
||||
else:
|
||||
return self.__class__(self[0] * other, self[1] * other, self[2] * other)
|
||||
return self.__class__(self.x // other, self.y // other, self.z // other)
|
||||
|
||||
def __mod__(self, other: int | float) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] % other, self[1] % other)
|
||||
else:
|
||||
return self.__class__(self[0] % other, self[1] % other, self[2] % other)
|
||||
def __truediv__(self, other):
|
||||
return self // other
|
||||
|
||||
def __floordiv__(self, other: int | float) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] // other, self[1] // other)
|
||||
def __gt__(self, other):
|
||||
if self.z is None:
|
||||
return self.x > other.x and self.y > other.y
|
||||
else:
|
||||
return self.__class__(self[0] // other, self[1] // other, self[2] // other)
|
||||
return self.x > other.x and self.y > other.y and self.z > other.z
|
||||
|
||||
def __truediv__(self, other: int | float) -> Coordinate:
|
||||
if self[2] is None:
|
||||
return self.__class__(self[0] / other, self[1] / other)
|
||||
def __ge__(self, other):
|
||||
if self.z is None:
|
||||
return self.x >= other.x and self.y >= other.y
|
||||
else:
|
||||
return self.__class__(self[0] / other, self[1] / other, self[2] / other)
|
||||
return self.x >= other.x and self.y >= other.y and self.z >= other.z
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.z is None:
|
||||
return self.x < other.x and self.y < other.y
|
||||
else:
|
||||
return self.x < other.x and self.y < other.y and self.z < other.z
|
||||
|
||||
def __le__(self, other):
|
||||
if self.z is None:
|
||||
return self.x <= other.x and self.y <= other.y
|
||||
else:
|
||||
return self.x <= other.x and self.y <= other.y and self.z <= other.z
|
||||
|
||||
def __str__(self):
|
||||
if self[2] is None:
|
||||
return "({},{})".format(self[0], self[1])
|
||||
if self.z is None:
|
||||
return "(%d,%d)" % (self.x, self.y)
|
||||
else:
|
||||
return "({},{},{})".format(self[0], self[1], self[2])
|
||||
return "(%d,%d,%d)" % (self.x, self.y, self.z)
|
||||
|
||||
def __repr__(self):
|
||||
if self[2] is None:
|
||||
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
|
||||
if self.z is None:
|
||||
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self.x, self.y)
|
||||
else:
|
||||
return "{}(x={}, y={}, z={})".format(
|
||||
return "%s(x=%d, y=%d, z=%d)" % (
|
||||
self.__class__.__name__,
|
||||
self[0],
|
||||
self[1],
|
||||
self[2],
|
||||
self.x,
|
||||
self.y,
|
||||
self.z,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls,
|
||||
from_x: int | float,
|
||||
to_x: int | float,
|
||||
from_y: int | float,
|
||||
to_y: int | float,
|
||||
from_z: int | float = None,
|
||||
to_z: int | float = None,
|
||||
step: int | float = 1,
|
||||
from_x: int,
|
||||
to_x: int,
|
||||
from_y: int,
|
||||
to_y: int,
|
||||
from_z: int = None,
|
||||
to_z: int = None,
|
||||
) -> List[Coordinate]:
|
||||
if from_z is None or to_z is None:
|
||||
return [cls(x, y) for x in range(from_x, to_x + step, step) for y in range(from_y, to_y + step, step)]
|
||||
return [
|
||||
cls(x, y)
|
||||
for x in range(from_x, to_x + 1)
|
||||
for y in range(from_y, to_y + 1)
|
||||
]
|
||||
else:
|
||||
return [
|
||||
cls(x, y, z)
|
||||
for x in range(from_x, to_x + step, step)
|
||||
for y in range(from_y, to_y + step, step)
|
||||
for z in range(from_z, to_z + step, step)
|
||||
for x in range(from_x, to_x + 1)
|
||||
for y in range(from_y, to_y + 1)
|
||||
for z in range(from_z, to_z + 1)
|
||||
]
|
||||
|
||||
|
||||
class HexCoordinate(Coordinate):
|
||||
"""
|
||||
https://www.redblobgames.com/grids/hexagons/#coordinates-cube
|
||||
Treat as 3d Coordinate
|
||||
+y -x +z
|
||||
y x z
|
||||
yxz
|
||||
z x y
|
||||
-z +x -y
|
||||
"""
|
||||
|
||||
neighbour_vectors = {
|
||||
"ne": Coordinate(-1, 0, 1),
|
||||
"nw": Coordinate(-1, 1, 0),
|
||||
"e": Coordinate(0, -1, 1),
|
||||
"w": Coordinate(0, 1, -1),
|
||||
"sw": Coordinate(1, 0, -1),
|
||||
"se": Coordinate(1, -1, 0),
|
||||
}
|
||||
|
||||
def __init__(self, x: int, y: int, z: int):
|
||||
assert (x + y + z) == 0
|
||||
super(HexCoordinate, self).__init__(x, y, z)
|
||||
|
||||
def get_length(self) -> int:
|
||||
return (abs(self.x) + abs(self.y) + abs(self.z)) // 2
|
||||
|
||||
def getDistanceTo(
|
||||
self,
|
||||
target: Coordinate,
|
||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
||||
includeDiagonals: bool = True,
|
||||
) -> Union[int, float]:
|
||||
# includeDiagonals makes no sense in a hex grid, it's just here for signature reasons
|
||||
if algorithm == DistanceAlgorithm.MANHATTAN:
|
||||
return (self - target).get_length()
|
||||
|
||||
def getNeighbours(
|
||||
self,
|
||||
includeDiagonal: bool = True,
|
||||
minX: int = -inf,
|
||||
minY: int = -inf,
|
||||
maxX: int = inf,
|
||||
maxY: int = inf,
|
||||
minZ: int = -inf,
|
||||
maxZ: int = inf,
|
||||
) -> list[Coordinate]:
|
||||
# includeDiagonals makes no sense in a hex grid, it's just here for signature reasons
|
||||
return [
|
||||
self + x
|
||||
for x in self.neighbour_vectors.values()
|
||||
if minX <= (self + x).x <= maxX
|
||||
and minY <= (self + x).y <= maxY
|
||||
and minZ <= (self + x).z <= maxZ
|
||||
]
|
||||
|
||||
|
||||
HexCoordinateR = HexCoordinate
|
||||
|
||||
|
||||
class HexCoordinateF(HexCoordinate):
|
||||
"""
|
||||
https://www.redblobgames.com/grids/hexagons/#coordinates-cube
|
||||
Treat as 3d Coordinate
|
||||
+y -x
|
||||
y x
|
||||
-z z yxz z +z
|
||||
x y
|
||||
+x -y
|
||||
"""
|
||||
|
||||
neighbour_vectors = {
|
||||
"ne": Coordinate(-1, 0, 1),
|
||||
"nw": Coordinate(0, 1, -1),
|
||||
"n": Coordinate(-1, 1, 0),
|
||||
"s": Coordinate(1, -1, 0),
|
||||
"sw": Coordinate(1, 0, -1),
|
||||
"se": Coordinate(0, -1, 1),
|
||||
}
|
||||
|
||||
def __init__(self, x: int, y: int, z: int):
|
||||
super(HexCoordinateF, self).__init__(x, y, z)
|
||||
|
||||
|
||||
class Shape:
|
||||
def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
|
||||
"""
|
||||
@ -330,7 +453,9 @@ class Shape:
|
||||
|
||||
def __len__(self):
|
||||
if not self.mode_3d:
|
||||
return (self.bottom_right.x - self.top_left.x + 1) * (self.bottom_right.y - self.top_left.y + 1)
|
||||
return (self.bottom_right.x - self.top_left.x + 1) * (
|
||||
self.bottom_right.y - self.top_left.y + 1
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(self.bottom_right.x - self.top_left.x + 1)
|
||||
@ -347,23 +472,43 @@ class Shape:
|
||||
|
||||
if not self.mode_3d:
|
||||
intersect_top_left = Coordinate(
|
||||
self.top_left.x if self.top_left.x > other.top_left.x else other.top_left.x,
|
||||
self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y,
|
||||
self.top_left.x
|
||||
if self.top_left.x > other.top_left.x
|
||||
else other.top_left.x,
|
||||
self.top_left.y
|
||||
if self.top_left.y > other.top_left.y
|
||||
else other.top_left.y,
|
||||
)
|
||||
intersect_bottom_right = Coordinate(
|
||||
self.bottom_right.x if self.bottom_right.x < other.bottom_right.x else other.bottom_right.x,
|
||||
self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y,
|
||||
self.bottom_right.x
|
||||
if self.bottom_right.x < other.bottom_right.x
|
||||
else other.bottom_right.x,
|
||||
self.bottom_right.y
|
||||
if self.bottom_right.y < other.bottom_right.y
|
||||
else other.bottom_right.y,
|
||||
)
|
||||
else:
|
||||
intersect_top_left = Coordinate(
|
||||
self.top_left.x if self.top_left.x > other.top_left.x else other.top_left.x,
|
||||
self.top_left.y if self.top_left.y > other.top_left.y else other.top_left.y,
|
||||
self.top_left.z if self.top_left.z > other.top_left.z else other.top_left.z,
|
||||
self.top_left.x
|
||||
if self.top_left.x > other.top_left.x
|
||||
else other.top_left.x,
|
||||
self.top_left.y
|
||||
if self.top_left.y > other.top_left.y
|
||||
else other.top_left.y,
|
||||
self.top_left.z
|
||||
if self.top_left.z > other.top_left.z
|
||||
else other.top_left.z,
|
||||
)
|
||||
intersect_bottom_right = Coordinate(
|
||||
self.bottom_right.x if self.bottom_right.x < other.bottom_right.x else other.bottom_right.x,
|
||||
self.bottom_right.y if self.bottom_right.y < other.bottom_right.y else other.bottom_right.y,
|
||||
self.bottom_right.z if self.bottom_right.z < other.bottom_right.z else other.bottom_right.z,
|
||||
self.bottom_right.x
|
||||
if self.bottom_right.x < other.bottom_right.x
|
||||
else other.bottom_right.x,
|
||||
self.bottom_right.y
|
||||
if self.bottom_right.y < other.bottom_right.y
|
||||
else other.bottom_right.y,
|
||||
self.bottom_right.z
|
||||
if self.bottom_right.z < other.bottom_right.z
|
||||
else other.bottom_right.z,
|
||||
)
|
||||
|
||||
if intersect_top_left <= intersect_bottom_right:
|
||||
@ -375,16 +520,6 @@ class Shape:
|
||||
def __rand__(self, other):
|
||||
return self.intersection(other)
|
||||
|
||||
def __contains__(self, item: Coordinate) -> bool:
|
||||
if not self.mode_3d:
|
||||
return self.top_left.x <= item.x <= self.bottom_right.x and self.top_left.y <= item.y <= self.bottom_right.y
|
||||
else:
|
||||
return (
|
||||
self.top_left.x <= item.x <= self.bottom_right.x
|
||||
and self.top_left.y <= item.y <= self.bottom_right.y
|
||||
and self.top_left.z <= item.z <= self.bottom_right.z
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s(%s -> %s)" % (
|
||||
self.__class__.__name__,
|
||||
@ -400,9 +535,9 @@ class Shape:
|
||||
)
|
||||
|
||||
|
||||
class Rectangle(Shape):
|
||||
class Square(Shape):
|
||||
def __init__(self, top_left, bottom_right):
|
||||
super(Rectangle, self).__init__(top_left, bottom_right)
|
||||
super(Square, self).__init__(top_left, bottom_right)
|
||||
self.mode_3d = False
|
||||
|
||||
|
||||
@ -411,131 +546,3 @@ class Cube(Shape):
|
||||
if top_left.z is None or bottom_right.z is None:
|
||||
raise ValueError("Both Coordinates need to be 3D")
|
||||
super(Cube, self).__init__(top_left, bottom_right)
|
||||
|
||||
|
||||
# FIXME: Line could probably also just be a subclass of Shape
|
||||
class Line:
|
||||
def __init__(self, start: Coordinate, end: Coordinate):
|
||||
if start[2] is not None or end[2] is not None:
|
||||
raise NotImplementedError("3D Lines are hard(er)")
|
||||
self.start, self.end = minmax(start, end)
|
||||
|
||||
def is_horizontal(self) -> bool:
|
||||
return self.start[1] == self.end[1]
|
||||
|
||||
def is_vertical(self) -> bool:
|
||||
return self.start[0] == self.end[0]
|
||||
|
||||
def connects_to(self, other: Line) -> bool:
|
||||
return self.start == other.start or self.start == other.end or self.end == other.start or self.end == other.end
|
||||
|
||||
def intersects(self, other: Line, strict: bool = True) -> bool:
|
||||
try:
|
||||
self.get_intersection(other, strict=strict)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def get_intersection(self, other: Line, strict: bool = True) -> Coordinate:
|
||||
xdiff = (self.start[0] - self.end[0], other.start[0] - other.end[0])
|
||||
ydiff = (self.start[1] - self.end[1], other.start[1] - other.end[1])
|
||||
|
||||
def det(a, b):
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
|
||||
div = det(xdiff, ydiff)
|
||||
if div == 0:
|
||||
raise ValueError("lines do not intersect")
|
||||
|
||||
d = (det(self.start, self.end), det(other.start, other.end))
|
||||
x = det(d, xdiff) / div
|
||||
y = det(d, ydiff) / div
|
||||
ret = Coordinate(x, y)
|
||||
|
||||
if not strict:
|
||||
return ret
|
||||
else:
|
||||
if ret in self and ret in other:
|
||||
return ret
|
||||
else:
|
||||
raise ValueError("intersection out of bounds")
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.start, self.end))
|
||||
|
||||
def __eq__(self, other: Line) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __lt__(self, other: Line) -> bool:
|
||||
return self.start < other.start
|
||||
|
||||
def __contains__(self, point: Coordinate | tuple) -> bool:
|
||||
return isclose(
|
||||
self.start.getDistanceTo(self.end),
|
||||
self.start.getDistanceTo(point) + self.end.getDistanceTo(point),
|
||||
)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return int(self.start.getDistanceTo(self.end))
|
||||
|
||||
def __str__(self):
|
||||
return f"Line({self.start} -> {self.end})"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
class Polygon:
|
||||
def __init__(self, points: list[Coordinate]) -> None:
|
||||
"""points have to be in (counter)clockwise order, not repeating the first coordinate"""
|
||||
if len(set(points)) != len(points):
|
||||
raise ValueError("Polygon contains repeated points")
|
||||
|
||||
self.points = points
|
||||
self.lines = set()
|
||||
for i in range(len(points) - 1):
|
||||
self.lines.add(Line(points[i], points[i + 1]))
|
||||
self.lines.add(Line(points[-1], points[0]))
|
||||
|
||||
def get_circumference(self) -> float:
|
||||
return sum(len(x) for x in self.lines)
|
||||
|
||||
def get_area(self) -> float:
|
||||
S = 0
|
||||
for i in range(len(self.points)):
|
||||
S += (
|
||||
self.points[i].x * self.points[(i + 1) % len(self.points)].y
|
||||
- self.points[(i + 1) % len(self.points)].x * self.points[i].y
|
||||
)
|
||||
|
||||
return abs(S) / 2
|
||||
|
||||
def decompose(self) -> Iterable[Rectangle]:
|
||||
points_left = list(self.points)
|
||||
|
||||
def flip(point: Coordinate):
|
||||
if point in points_left:
|
||||
points_left.remove(point)
|
||||
else:
|
||||
points_left.append(point)
|
||||
|
||||
while points_left:
|
||||
pk, pl, pm = None, None, None
|
||||
for c in sorted(points_left, key=lambda p: (p[1], p[0])):
|
||||
if pk is None:
|
||||
pk = c
|
||||
continue
|
||||
|
||||
if pl is None:
|
||||
pl = c
|
||||
continue
|
||||
|
||||
if pk.x <= c.x < pl.x and pk.y < c.y:
|
||||
pm = c
|
||||
break
|
||||
|
||||
flip(pk)
|
||||
flip(pl)
|
||||
flip(Coordinate(pk.x, pm.y))
|
||||
flip(Coordinate(pl.x, pm.y))
|
||||
yield Rectangle(pk, Coordinate(pl.x, pm.y))
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from collections import deque
|
||||
from .aoc_ocr import convert_array_6
|
||||
from .coordinate import Coordinate, DistanceAlgorithm, Shape
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from heapq import heappop, heappush
|
||||
from math import inf
|
||||
from typing import Any, Dict, List, Iterable, Mapping
|
||||
from typing import Any, Dict, List
|
||||
|
||||
OFF = False
|
||||
ON = True
|
||||
@ -48,19 +46,19 @@ class Grid:
|
||||
|
||||
def __trackBoundaries(self, pos: Coordinate):
|
||||
if self.minX is None:
|
||||
self.minX, self.maxX, self.minY, self.maxY = pos[0], pos[0], pos[1], pos[1]
|
||||
self.minX, self.maxX, self.minY, self.maxY = pos.x, pos.x, pos.y, pos.y
|
||||
else:
|
||||
self.minX = pos[0] if pos[0] < self.minX else self.minX
|
||||
self.minY = pos[1] if pos[1] < self.minY else self.minY
|
||||
self.maxX = pos[0] if pos[0] > self.maxX else self.maxX
|
||||
self.maxY = pos[1] if pos[1] > self.maxY else self.maxY
|
||||
self.minX = pos.x if pos.x < self.minX else self.minX
|
||||
self.minY = pos.y if pos.y < self.minY else self.minY
|
||||
self.maxX = pos.x if pos.x > self.maxX else self.maxX
|
||||
self.maxY = pos.y if pos.y > self.maxY else self.maxY
|
||||
|
||||
if self.mode3D:
|
||||
if self.minZ is None:
|
||||
self.minZ = self.maxZ = pos[2]
|
||||
self.minZ = self.maxZ = pos.z
|
||||
else:
|
||||
self.minZ = pos[2] if pos[2] < self.minZ else self.minZ
|
||||
self.maxZ = pos[2] if pos[2] > self.maxZ else self.maxZ
|
||||
self.minZ = pos.z if pos.z < self.minZ else self.minZ
|
||||
self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ
|
||||
|
||||
def recalcBoundaries(self) -> None:
|
||||
self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = (
|
||||
@ -100,12 +98,6 @@ class Grid:
|
||||
else:
|
||||
return range(self.minZ - pad, self.maxZ + pad + 1)
|
||||
|
||||
def get_column(self, column: int) -> list[Any]:
|
||||
return [self.get(Coordinate(column, y)) for y in self.rangeY()]
|
||||
|
||||
def get_row(self, row: int) -> list[Any]:
|
||||
return [self.get(Coordinate(x, row)) for x in self.rangeX()]
|
||||
|
||||
def toggle(self, pos: Coordinate):
|
||||
if pos in self.__grid:
|
||||
del self.__grid[pos]
|
||||
@ -123,7 +115,7 @@ class Grid:
|
||||
self.toggle(Coordinate(x, y, z))
|
||||
|
||||
def set(self, pos: Coordinate, value: Any = True) -> Any:
|
||||
if pos[2] is not None:
|
||||
if pos.z is not None:
|
||||
self.mode3D = True
|
||||
|
||||
if (value == self.__default) and pos in self.__grid:
|
||||
@ -157,13 +149,13 @@ class Grid:
|
||||
return self.set(pos, self.get(pos) / value)
|
||||
|
||||
def add_shape(self, shape: Shape, value: int | float = 1) -> None:
|
||||
for x in range(shape.top_left[0], shape.bottom_right[0] + 1):
|
||||
for y in range(shape.top_left[1], shape.bottom_right[1] + 1):
|
||||
for x in range(shape.top_left.x, shape.bottom_right.x + 1):
|
||||
for y in range(shape.top_left.y, shape.bottom_right.y + 1):
|
||||
if not shape.mode_3d:
|
||||
pos = Coordinate(x, y)
|
||||
self.set(pos, self.get(pos) + value)
|
||||
else:
|
||||
for z in range(shape.top_left[2], shape.bottom_right[2] + 1):
|
||||
for z in range(shape.top_left.z, shape.bottom_right.z + 1):
|
||||
pos = Coordinate(x, y, z)
|
||||
self.set(pos, self.get(pos) + value)
|
||||
|
||||
@ -173,11 +165,6 @@ class Grid:
|
||||
def getOnCount(self) -> int:
|
||||
return len(self.__grid)
|
||||
|
||||
def find(self, value: Any) -> Iterable[Coordinate]:
|
||||
for k, v in self.__grid.items():
|
||||
if v == value:
|
||||
yield k
|
||||
|
||||
def count(self, value: Any) -> int:
|
||||
return list(self.__grid.values()).count(value)
|
||||
|
||||
@ -207,41 +194,29 @@ class Grid:
|
||||
def isCorner(self, pos: Coordinate) -> bool:
|
||||
return pos in self.getCorners()
|
||||
|
||||
def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool:
|
||||
def isWithinBoundaries(self, pos: Coordinate) -> bool:
|
||||
if self.mode3D:
|
||||
return (
|
||||
self.minX + pad <= pos[0] <= self.maxX - pad
|
||||
and self.minY + pad <= pos[1] <= self.maxY - pad
|
||||
and self.minZ + pad <= pos[2] <= self.maxZ - pad
|
||||
self.minX <= pos.x <= self.maxX
|
||||
and self.minY <= pos.y <= self.maxY
|
||||
and self.minZ <= pos.z <= self.maxZ
|
||||
)
|
||||
else:
|
||||
return self.minX + pad <= pos[0] <= self.maxX - pad and self.minY + pad <= pos[1] <= self.maxY - pad
|
||||
return self.minX <= pos.x <= self.maxX and self.minY <= pos.y <= self.maxY
|
||||
|
||||
def getActiveCells(self, x: int = None, y: int = None, z: int = None) -> Iterable[Coordinate]:
|
||||
def getActiveCells(
|
||||
self, x: int = None, y: int = None, z: int = None
|
||||
) -> List[Coordinate]:
|
||||
if x is not None or y is not None or z is not None:
|
||||
return (
|
||||
return [
|
||||
c
|
||||
for c in self.__grid.keys()
|
||||
if (c[0] == x if x is not None else True)
|
||||
and (c[1] == y if y is not None else True)
|
||||
and (c[2] == z if z is not None else True)
|
||||
)
|
||||
if (c.x == x if x is not None else True)
|
||||
and (c.y == y if y is not None else True)
|
||||
and (c.z == z if z is not None else True)
|
||||
]
|
||||
else:
|
||||
return self.__grid.keys()
|
||||
|
||||
def getRegion(self, start: Coordinate, includeDiagonal: bool = False) -> Iterable[Coordinate]:
|
||||
start_value = self.get(start)
|
||||
queue = deque()
|
||||
queue.append(start)
|
||||
visited = set()
|
||||
while queue:
|
||||
next_coord = queue.popleft()
|
||||
if next_coord in visited or not self.isWithinBoundaries(next_coord) or self.get(next_coord) != start_value:
|
||||
continue
|
||||
visited.add(next_coord)
|
||||
yield next_coord
|
||||
for n in self.getNeighboursOf(next_coord, includeDefault=True, includeDiagonal=includeDiagonal):
|
||||
queue.append(n)
|
||||
return list(self.__grid.keys())
|
||||
|
||||
def getActiveRegion(
|
||||
self,
|
||||
@ -285,7 +260,7 @@ class Grid:
|
||||
pos: Coordinate,
|
||||
includeDefault: bool = False,
|
||||
includeDiagonal: bool = True,
|
||||
) -> Iterable[Coordinate]:
|
||||
) -> List[Coordinate]:
|
||||
neighbours = pos.getNeighbours(
|
||||
includeDiagonal=includeDiagonal,
|
||||
minX=self.minX,
|
||||
@ -326,47 +301,47 @@ class Grid:
|
||||
if mode == GridTransformation.ROTATE_X:
|
||||
shift_z = self.maxY
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[0], c[2], -c[1]), v)
|
||||
self.set(Coordinate(c.x, c.z, -c.y), v)
|
||||
self.shift(shift_z=shift_z)
|
||||
elif mode == GridTransformation.ROTATE_Y:
|
||||
shift_x = self.maxX
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(-c[2], c[1], c[0]), v)
|
||||
self.set(Coordinate(-c.z, c.y, c.x), v)
|
||||
self.shift(shift_x=shift_x)
|
||||
elif mode == GridTransformation.ROTATE_Z:
|
||||
shift_x = self.maxX
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(-c[1], c[0], c[2]), v)
|
||||
self.set(Coordinate(-c.y, c.x, c.z), v)
|
||||
self.shift(shift_x=shift_x)
|
||||
elif mode == GridTransformation.COUNTER_ROTATE_X:
|
||||
shift_y = self.maxY
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[0], -c[2], c[1]), v)
|
||||
self.set(Coordinate(c.x, -c.z, c.y), v)
|
||||
self.shift(shift_y=shift_y)
|
||||
elif mode == GridTransformation.COUNTER_ROTATE_Y:
|
||||
shift_z = self.maxZ
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[2], c[1], -c[0]), v)
|
||||
self.set(Coordinate(c.z, c.y, -c.x), v)
|
||||
self.shift(shift_z=shift_z)
|
||||
elif mode == GridTransformation.COUNTER_ROTATE_Z:
|
||||
shift_y = self.maxY
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[1], -c[0], c[2]), v)
|
||||
self.set(Coordinate(c.y, -c.x, c.z), v)
|
||||
self.shift(shift_y=shift_y)
|
||||
elif mode == GridTransformation.FLIP_X:
|
||||
shift_x = self.maxX
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(-c[0], c[1], c[2]), v)
|
||||
self.set(Coordinate(-c.x, c.y, c.z), v)
|
||||
self.shift(shift_x=shift_x)
|
||||
elif mode == GridTransformation.FLIP_Y:
|
||||
shift_y = self.maxY
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[0], -c[1], c[2]), v)
|
||||
self.set(Coordinate(c.x, -c.y, c.z), v)
|
||||
self.shift(shift_y=shift_y)
|
||||
elif mode == GridTransformation.FLIP_Z:
|
||||
shift_z = self.maxZ
|
||||
for c, v in coords.items():
|
||||
self.set(Coordinate(c[0], c[1], -c[2]), v)
|
||||
self.set(Coordinate(c.x, c.y, -c.z), v)
|
||||
self.shift(shift_z=shift_z)
|
||||
else:
|
||||
raise NotImplementedError(mode)
|
||||
@ -382,9 +357,9 @@ class Grid:
|
||||
self.__grid = {}
|
||||
for c, v in coords.items():
|
||||
if self.mode3D:
|
||||
nc = Coordinate(c[0] + shift_x, c[1] + shift_y, c[2] + shift_z)
|
||||
nc = Coordinate(c.x + shift_x, c.y + shift_y, c.z + shift_z)
|
||||
else:
|
||||
nc = Coordinate(c[0] + shift_x, c[1] + shift_y)
|
||||
nc = Coordinate(c.x + shift_x, c.y + shift_y)
|
||||
self.set(nc, v)
|
||||
|
||||
def shift_zero(self, recalc: bool = True):
|
||||
@ -421,7 +396,9 @@ class Grid:
|
||||
if c in came_from and self.get(c) in walls:
|
||||
continue
|
||||
came_from[c] = current
|
||||
if c == pos_to or (stop_at_first is not None and self.get(c) == stop_at_first):
|
||||
if c == pos_to or (
|
||||
stop_at_first is not None and self.get(c) == stop_at_first
|
||||
):
|
||||
pos_to = c
|
||||
found_end = True
|
||||
break
|
||||
@ -468,7 +445,9 @@ class Grid:
|
||||
if currentCoord == pos_to:
|
||||
break
|
||||
|
||||
for neighbour in self.getNeighboursOf(currentCoord, includeDefault=True, includeDiagonal=includeDiagonal):
|
||||
for neighbour in self.getNeighboursOf(
|
||||
currentCoord, includeDefault=True, includeDiagonal=includeDiagonal
|
||||
):
|
||||
if self.get(neighbour) in walls or neighbour in closedNodes:
|
||||
continue
|
||||
|
||||
@ -477,7 +456,9 @@ class Grid:
|
||||
elif not includeDiagonal:
|
||||
neighbourDist = 1
|
||||
else:
|
||||
neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal)
|
||||
neighbourDist = currentCoord.getDistanceTo(
|
||||
neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal
|
||||
)
|
||||
|
||||
targetDist = neighbour.getDistanceTo(pos_to)
|
||||
f_cost = targetDist + neighbourDist + currentNode[1]
|
||||
@ -511,13 +492,17 @@ class Grid:
|
||||
to_z: int = None,
|
||||
) -> "Grid":
|
||||
if self.mode3D and (from_z is None or to_z is None):
|
||||
raise ValueError("sub_grid() on mode3d Grids requires from_z and to_z to be set")
|
||||
raise ValueError(
|
||||
"sub_grid() on mode3d Grids requires from_z and to_z to be set"
|
||||
)
|
||||
count_x, count_y, count_z = 0, 0, 0
|
||||
new_grid = Grid(self.__default)
|
||||
for x in range(from_x, to_x + 1):
|
||||
for y in range(from_y, to_y + 1):
|
||||
if not self.mode3D:
|
||||
new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y)))
|
||||
new_grid.set(
|
||||
Coordinate(count_x, count_y), self.get(Coordinate(x, y))
|
||||
)
|
||||
else:
|
||||
for z in range(from_z, to_z + 1):
|
||||
new_grid.set(
|
||||
@ -584,14 +569,20 @@ class Grid:
|
||||
def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0):
|
||||
return convert_array_6(
|
||||
[
|
||||
["#" if self.get(Coordinate(x + x_shift, y + y_shift)) else "." for x in self.rangeX()]
|
||||
[
|
||||
"#" if self.get(Coordinate(x + x_shift, y + y_shift)) else "."
|
||||
for x in self.rangeX()
|
||||
]
|
||||
for y in self.rangeY()
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self, true_char: str = "#", false_char: str = "."):
|
||||
return "/".join(
|
||||
"".join(true_char if self.get(Coordinate(x, y)) else false_char for x in range(self.minX, self.maxX + 1))
|
||||
"".join(
|
||||
true_char if self.get(Coordinate(x, y)) else false_char
|
||||
for x in range(self.minX, self.maxX + 1)
|
||||
)
|
||||
for y in range(self.minY, self.maxY + 1)
|
||||
)
|
||||
|
||||
@ -607,7 +598,11 @@ class Grid:
|
||||
) -> "Grid":
|
||||
if translate is None:
|
||||
translate = {}
|
||||
if true_char is not None and True not in translate.values() and true_char not in translate:
|
||||
if (
|
||||
true_char is not None
|
||||
and True not in translate.values()
|
||||
and true_char not in translate
|
||||
):
|
||||
translate[true_char] = true_value if true_value is not None else True
|
||||
|
||||
ret = cls(default=default)
|
||||
@ -625,56 +620,17 @@ class Grid:
|
||||
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def from_data(
|
||||
cls,
|
||||
data: Iterable[Iterable],
|
||||
default: Any = False,
|
||||
translate: Mapping[str, Any] = None,
|
||||
gen_3d: bool = False,
|
||||
) -> Grid:
|
||||
"""
|
||||
Every entry in data will be treated as row, every entry in data[entry] will be a separate column.
|
||||
gen_3d = True will just add z=0 to every Coordinate
|
||||
translate is used on every data[entry] and if present as key, its value will be used instead
|
||||
a value in translate can be a function with the following signature: def translate(value: Any) -> Any
|
||||
a key in translate is either a string of len 1 or it will be treated as regexp
|
||||
if multiple regexp match, the first encountered wins
|
||||
if there is a key that matches the entry it wins over any mathing regexp
|
||||
"""
|
||||
grid = cls(default=default)
|
||||
def __eq__(self, other: Grid) -> bool:
|
||||
if not isinstance(other, Grid):
|
||||
return False
|
||||
|
||||
regex_in_translate = False
|
||||
if translate is not None:
|
||||
for k in translate:
|
||||
if len(k) > 1:
|
||||
regex_in_translate = True
|
||||
other_active = set(other.getActiveCells())
|
||||
for c, v in self.__grid.items():
|
||||
if other.get(c) != v:
|
||||
return False
|
||||
other_active.remove(c)
|
||||
|
||||
for y, row in enumerate(data):
|
||||
for x, col in enumerate(row):
|
||||
if translate is not None and col in translate:
|
||||
if isinstance(translate[col], Callable):
|
||||
col = translate[col](col)
|
||||
else:
|
||||
col = translate[col]
|
||||
elif regex_in_translate:
|
||||
for k, v in translate.items():
|
||||
if len(k) == 1:
|
||||
continue
|
||||
if other_active:
|
||||
return False
|
||||
|
||||
if re.search(k, col):
|
||||
if isinstance(v, Callable):
|
||||
col = translate[k](col)
|
||||
else:
|
||||
col = v
|
||||
break
|
||||
|
||||
if gen_3d:
|
||||
grid.set(Coordinate(x, y, 0), col)
|
||||
else:
|
||||
grid.set(Coordinate(x, y), col)
|
||||
|
||||
return grid
|
||||
|
||||
def __hash__(self):
|
||||
return hash(frozenset(self.__grid.items()))
|
||||
return True
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
def factorial(n: int, start: int = 1) -> int:
|
||||
import math
|
||||
|
||||
|
||||
def factorial(n: int) -> int:
|
||||
"""
|
||||
n! = 1 * 2 * 3 * 4 * ... * n
|
||||
1, 1, 2, 6, 24, 120, 720, ...
|
||||
|
||||
If you're looking for efficiency with start == 1, just use math.factorial(n)
|
||||
which takes just 25% of the compute time on average, but this is the fastest
|
||||
pure python implementation I could come up with and it allows for partial
|
||||
multiplications, like 5 * 6 * 7 * 8 * .... * 17
|
||||
"""
|
||||
if start == n:
|
||||
return n
|
||||
if n - start == 1:
|
||||
return n * start
|
||||
|
||||
middle = start + (n - start) // 2
|
||||
return factorial(middle, start) * factorial(n, middle + 1)
|
||||
return math.factorial(n)
|
||||
|
||||
|
||||
def fibonacci(n: int) -> int:
|
||||
@ -46,10 +38,3 @@ def pentagonal(n: int) -> int:
|
||||
0, 1, 5, 12, 22, 35, ...
|
||||
"""
|
||||
return ((3 * n * n) - n) // 2
|
||||
|
||||
|
||||
def hexagonal(n: int) -> int:
|
||||
if n == 1:
|
||||
return 1
|
||||
|
||||
return n * 2 + (n - 1) * 2 + (n - 2) * 2 + hexagonal(n - 1)
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from time import sleep
|
||||
from .schedule import Scheduler
|
||||
from .simplesocket import ClientSocket
|
||||
from base64 import b64encode
|
||||
from collections import deque
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from time import sleep
|
||||
from typing import Callable
|
||||
|
||||
|
||||
@ -157,15 +155,6 @@ class ServerMessage(str, Enum):
|
||||
ERR_NOOPERHOST = "491"
|
||||
ERR_UMODEUNKNOWNFLAG = "501"
|
||||
ERR_USERSDONTMATCH = "502"
|
||||
RPL_LOGGEDIN = "900"
|
||||
RPL_LOGGEDOUT = "901"
|
||||
ERR_NICKLOCKED = "902"
|
||||
RPL_SASLSUCCESS = "903"
|
||||
ERR_SASLFAIL = "904"
|
||||
ERR_SASLTOOLONG = "905"
|
||||
ERR_SASLABORTED = "906"
|
||||
ERR_SASLALREADY = "907"
|
||||
RPL_SASLMECHS = "908"
|
||||
MSG_NICK = "NICK"
|
||||
MSG_TOPIC = "TOPIC"
|
||||
MSG_MODE = "MODE"
|
||||
@ -232,30 +221,20 @@ class Client:
|
||||
nick: str,
|
||||
username: str,
|
||||
realname: str = "Python Bot",
|
||||
sasl_password: str = None,
|
||||
):
|
||||
self.__connected = False
|
||||
self.__send_queue = deque()
|
||||
self.__userlist = {}
|
||||
self.__channellist = {}
|
||||
self.__server_socket = ClientSocket(server, port)
|
||||
|
||||
if sasl_password is not None:
|
||||
self.__server_socket.sendline("CAP REQ :sasl")
|
||||
self.__server_socket.sendline("USER %s ignore ignore :%s" % (username, realname))
|
||||
self.__server_socket.sendline(
|
||||
"USER %s ignore ignore :%s" % (username, realname)
|
||||
)
|
||||
self.__server_socket.sendline("NICK %s" % nick)
|
||||
if sasl_password is not None:
|
||||
self.__server_socket.sendline("AUTHENTICATE PLAIN")
|
||||
auth_string = b64encode(bytes("%s\0%s\0%s" % (nick, nick, sasl_password), "utf-8")).decode("ascii")
|
||||
self.__server_socket.sendline("AUTHENTICATE %s" % auth_string)
|
||||
|
||||
self.__server_caps = {"MAXLEN": 255}
|
||||
self.__function_register = {
|
||||
ServerMessage.RPL_WELCOME: [self.on_rpl_welcome],
|
||||
ServerMessage.RPL_TOPIC: [self.on_rpl_topic],
|
||||
ServerMessage.RPL_ISUPPORT: [self.on_rpl_isupport],
|
||||
ServerMessage.ERR_NICKNAMEINUSE: [self.on_err_nicknameinuse],
|
||||
ServerMessage.RPL_LOGGEDIN: [self.on_auth],
|
||||
ServerMessage.MSG_JOIN: [self.on_join],
|
||||
ServerMessage.MSG_PART: [self.on_part],
|
||||
ServerMessage.MSG_QUIT: [self.on_quit],
|
||||
@ -264,17 +243,6 @@ class Client:
|
||||
}
|
||||
self.receive()
|
||||
|
||||
def send_raw(self, msg: str):
|
||||
self.__send_queue.append(msg)
|
||||
|
||||
def send_queue(self):
|
||||
if not self.__connected or not self.__send_queue:
|
||||
return
|
||||
|
||||
msg = self.__send_queue.popleft()
|
||||
print(f"-> {msg}")
|
||||
self.__server_socket.sendline(msg)
|
||||
|
||||
def receive(self):
|
||||
while line := self.__server_socket.recvline():
|
||||
line = line.strip()
|
||||
@ -299,7 +267,10 @@ class Client:
|
||||
|
||||
if "!" in msg_from and msg_from not in self.__userlist:
|
||||
self.__userlist[msg_from] = User(msg_from)
|
||||
if self.__userlist[msg_from].nickname == self.__userlist[self.__my_user].nickname:
|
||||
if (
|
||||
self.__userlist[msg_from].nickname
|
||||
== self.__userlist[self.__my_user].nickname
|
||||
):
|
||||
del self.__userlist[self.__my_user]
|
||||
self.__my_user = msg_from
|
||||
|
||||
@ -313,14 +284,9 @@ class Client:
|
||||
else:
|
||||
self.__function_register[msg_type] = [func]
|
||||
|
||||
def on_auth(self, msg_from: str, msg_to: str, message: str):
|
||||
print("authed")
|
||||
self.__server_socket.sendline("CAP END")
|
||||
|
||||
def on_rpl_welcome(self, msg_from: str, msg_to: str, message: str):
|
||||
self.__my_user = message.split()[-1]
|
||||
self.__userlist[self.__my_user] = User(self.__my_user)
|
||||
self.__connected = True
|
||||
|
||||
def on_rpl_isupport(self, msg_from: str, msg_to: str, message: str):
|
||||
for cap in message.split():
|
||||
@ -344,7 +310,9 @@ class Client:
|
||||
|
||||
def on_nick(self, msg_from: str, msg_to: str, message: str):
|
||||
self.__userlist[msg_from].nick(msg_to)
|
||||
self.__userlist[self.__userlist[msg_from].identifier] = self.__userlist[msg_from]
|
||||
self.__userlist[self.__userlist[msg_from].identifier] = self.__userlist[
|
||||
msg_from
|
||||
]
|
||||
del self.__userlist[msg_from]
|
||||
|
||||
def on_join(self, msg_from: str, msg_to: str, message: str):
|
||||
@ -374,21 +342,21 @@ class Client:
|
||||
print(msg_from, msg_type, msg_to, message)
|
||||
|
||||
def nick(self, new_nick: str):
|
||||
self.send_raw("NICK %s" % new_nick)
|
||||
self.__server_socket.sendline("NICK %s" % new_nick)
|
||||
|
||||
def join(self, channel: str):
|
||||
self.send_raw("JOIN %s" % channel)
|
||||
self.__server_socket.sendline("JOIN %s" % channel)
|
||||
self.receive()
|
||||
|
||||
def part(self, channel: str):
|
||||
self.send_raw("PART %s" % channel)
|
||||
self.__server_socket.sendline("PART %s" % channel)
|
||||
self.receive()
|
||||
|
||||
def privmsg(self, target: str, message: str):
|
||||
self.send_raw("PRIVMSG %s :%s" % (target, message))
|
||||
self.__server_socket.sendline("PRIVMSG %s :%s" % (target, message))
|
||||
|
||||
def quit(self, message: str = "Elvis has left the building!"):
|
||||
self.send_raw("QUIT :%s" % message)
|
||||
self.__server_socket.sendline("QUIT :%s" % message)
|
||||
self.receive()
|
||||
self.__server_socket.close()
|
||||
|
||||
@ -421,9 +389,8 @@ class IrcBot(Client):
|
||||
nick: str,
|
||||
username: str,
|
||||
realname: str = "Python Bot",
|
||||
sasl_password: str = None,
|
||||
):
|
||||
super().__init__(server, port, nick, username, realname, sasl_password)
|
||||
super().__init__(server, port, nick, username, realname)
|
||||
self._scheduler = Scheduler()
|
||||
self._channel_commands = {}
|
||||
self._privmsg_commands = {}
|
||||
@ -448,8 +415,13 @@ class IrcBot(Client):
|
||||
if not message:
|
||||
return
|
||||
command = message.split()[0]
|
||||
if msg_to in self._channel_commands and command in self._channel_commands[msg_to]:
|
||||
self._channel_commands[msg_to][command](msg_from, " ".join(message.split()[1:]))
|
||||
if (
|
||||
msg_to in self._channel_commands
|
||||
and command in self._channel_commands[msg_to]
|
||||
):
|
||||
self._channel_commands[msg_to][command](
|
||||
msg_from, " ".join(message.split()[1:])
|
||||
)
|
||||
|
||||
if msg_to == self.getUser().nickname and command in self._privmsg_commands:
|
||||
self._privmsg_commands[command](msg_from, " ".join(message.split()[1:]))
|
||||
@ -457,6 +429,5 @@ class IrcBot(Client):
|
||||
def run(self):
|
||||
while True:
|
||||
self._scheduler.run_pending()
|
||||
self.send_queue()
|
||||
self.receive()
|
||||
sleep(0.01)
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
from math import factorial, comb
|
||||
from typing import Sized, Iterator
|
||||
|
||||
|
||||
def len_combinations(iterable: Sized, r: int) -> int:
|
||||
"""How many options will itertools.combinations(iterable, r) yield?"""
|
||||
n = len(iterable)
|
||||
if r > n:
|
||||
return 0
|
||||
else:
|
||||
return factorial(n) // factorial(r) // factorial(n - r)
|
||||
|
||||
|
||||
def len_permutations(iterable: Sized, r: int) -> int:
|
||||
"""How many options will itertools.permutations(iterable, r) yield?"""
|
||||
n = len(iterable)
|
||||
if r > n:
|
||||
return 0
|
||||
else:
|
||||
return factorial(n) // factorial(n - r)
|
||||
|
||||
|
||||
def combinations_of_sum(total_sum: int, length: int = None, min_value: int = 0) -> Iterator[tuple[int]]:
|
||||
if length is None:
|
||||
length = total_sum
|
||||
|
||||
if length == 1:
|
||||
yield (total_sum,)
|
||||
else:
|
||||
for value in range(min_value, total_sum + 1):
|
||||
for permutation in combinations_of_sum(total_sum - value, length - 1, min_value):
|
||||
yield (value,) + permutation
|
||||
|
||||
|
||||
def len_combinations_of_sum(total_sum: int, length: int = None, min_value: int = 0) -> int:
|
||||
"""
|
||||
How many options will combinations_of_sum(total_sum, length) yield?
|
||||
|
||||
No idea how to factor in min_value, yet, so if using min_value, the answer will always be too high
|
||||
"""
|
||||
return comb(total_sum + length - 1, total_sum)
|
||||
@ -6,8 +6,8 @@ from typing import Any
|
||||
@dataclass
|
||||
class Node:
|
||||
value: Any
|
||||
next: Node = None
|
||||
prev: Node = None
|
||||
next: "Node" = None
|
||||
prev: "Node" = None
|
||||
|
||||
|
||||
class LinkedList:
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
MAXINT32 = 2**32
|
||||
MAXINT64 = 2**64
|
||||
MAXINT32_SIGNED = 2**31
|
||||
MAXINT64_SIGNED = 2**63
|
||||
MAXINT32_UNSIGNED = MAXINT32
|
||||
MAXINT64_UNSIGNED = MAXINT64
|
||||
|
||||
|
||||
def round_half_up(number: int | float) -> int:
|
||||
@ -25,20 +16,3 @@ def get_factors(num: int) -> set:
|
||||
f.add(num // x)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def mul(ints: Iterable[int]) -> int:
|
||||
"""similar to sum(), just for multiplication"""
|
||||
ret = 1
|
||||
for x in ints:
|
||||
ret *= x
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def magnitude(value: int | float) -> int:
|
||||
return math.floor(math.log10(value))
|
||||
|
||||
|
||||
def concat(value1: int, value2: int) -> int:
|
||||
return value1 * (10 ** (1 + magnitude(value2))) + value2
|
||||
|
||||
@ -8,7 +8,6 @@ class StopWatch:
|
||||
stopped: int | None = None
|
||||
|
||||
def __init__(self, auto_start=True):
|
||||
self.total_elapsed = 0
|
||||
if auto_start:
|
||||
self.start()
|
||||
|
||||
@ -16,13 +15,11 @@ class StopWatch:
|
||||
self.started = perf_counter_ns()
|
||||
self.stopped = None
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> float:
|
||||
self.stopped = perf_counter_ns()
|
||||
self.total_elapsed += self.elapsed()
|
||||
return self.elapsed()
|
||||
|
||||
def reset(self):
|
||||
self.total_elapsed = 0
|
||||
self.start()
|
||||
reset = start
|
||||
|
||||
def elapsed(self) -> int:
|
||||
if self.stopped is None:
|
||||
@ -31,10 +28,10 @@ class StopWatch:
|
||||
return self.stopped - self.started
|
||||
|
||||
def elapsed_string(self) -> str:
|
||||
return human_readable_time_from_ns(self.total_elapsed)
|
||||
return human_readable_time_from_ns(self.elapsed())
|
||||
|
||||
def avg_elapsed(self, divider: int) -> float:
|
||||
return self.total_elapsed / divider
|
||||
return self.elapsed() / divider
|
||||
|
||||
def avg_string(self, divider: int) -> str:
|
||||
return human_readable_time_from_ns(int(self.avg_elapsed(divider)))
|
||||
|
||||
@ -2,6 +2,8 @@ import datetime
|
||||
import inspect
|
||||
import os.path
|
||||
import sys
|
||||
from fishhook import hook
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
|
||||
@ -44,7 +46,9 @@ class Dict(dict):
|
||||
self[k] = Dict(self[k])
|
||||
elif isinstance(self[k], list):
|
||||
for i in range(len(self[k])):
|
||||
if isinstance(self[k][i], dict) and not isinstance(self[k][i], Dict):
|
||||
if isinstance(self[k][i], dict) and not isinstance(
|
||||
self[k][i], Dict
|
||||
):
|
||||
self[k][i] = Dict(self[k][i])
|
||||
|
||||
def update(self, other: dict, **kwargs):
|
||||
@ -100,7 +104,14 @@ def minmax(*arr: Any) -> (Any, Any):
|
||||
else:
|
||||
return arr[0], arr[0]
|
||||
|
||||
return min(arr), max(arr)
|
||||
arr = set(arr)
|
||||
smallest = min(arr)
|
||||
biggest = max(arr)
|
||||
if smallest == biggest:
|
||||
arr.remove(smallest)
|
||||
biggest = max(arr)
|
||||
|
||||
return smallest, biggest
|
||||
|
||||
|
||||
def human_readable_time_from_delta(delta: datetime.timedelta) -> str:
|
||||
@ -109,16 +120,16 @@ def human_readable_time_from_delta(delta: datetime.timedelta) -> str:
|
||||
time_str += "%d day%s, " % (delta.days, "s" if delta.days > 1 else "")
|
||||
|
||||
if delta.seconds > 3600:
|
||||
time_str += "%02d hours, " % (delta.seconds // 3600)
|
||||
time_str += "%02d:" % (delta.seconds // 3600)
|
||||
else:
|
||||
time_str += ""
|
||||
time_str += "00:"
|
||||
|
||||
if delta.seconds % 3600 > 60:
|
||||
time_str += "%02d minutes, " % (delta.seconds % 3600 // 60)
|
||||
time_str += "%02d:" % (delta.seconds % 3600 // 60)
|
||||
else:
|
||||
time_str += ""
|
||||
time_str += "00:"
|
||||
|
||||
return time_str + "%02d seconds" % (delta.seconds % 60)
|
||||
return time_str + "%02d" % (delta.seconds % 60)
|
||||
|
||||
|
||||
def human_readable_time_from_ns(ns: int) -> str:
|
||||
@ -138,3 +149,51 @@ def human_readable_time_from_ns(ns: int) -> str:
|
||||
time_parts.insert(0, "%d%s" % (p, unit))
|
||||
if ns == 0:
|
||||
return ", ".join(time_parts)
|
||||
|
||||
|
||||
def cache(func):
|
||||
saved = {}
|
||||
|
||||
@wraps(func)
|
||||
def new_func(*args):
|
||||
if args in saved:
|
||||
return saved[args]
|
||||
|
||||
result = func(*args)
|
||||
saved[args] = result
|
||||
return result
|
||||
|
||||
return new_func
|
||||
|
||||
|
||||
@hook(list)
|
||||
def intersection(self, *args) -> list:
|
||||
ret = set(self).intersection(*args)
|
||||
return list(ret)
|
||||
|
||||
|
||||
@hook(list)
|
||||
def __and__(self, *args) -> list:
|
||||
return self.intersection(*args)
|
||||
|
||||
|
||||
@hook(str)
|
||||
def intersection(self, *args) -> str:
|
||||
ret = set(self).intersection(*args)
|
||||
return "".join(list(ret))
|
||||
|
||||
|
||||
@hook(str)
|
||||
def __and__(self, *args) -> str:
|
||||
return self.intersection(*args)
|
||||
|
||||
|
||||
@hook(int)
|
||||
def sum_digits(self) -> int:
|
||||
s = 0
|
||||
num = self
|
||||
while num > 0:
|
||||
s += num % 10
|
||||
num //= 10
|
||||
|
||||
return s
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
from math import ceil
|
||||
|
||||
|
||||
class Integer(int):
|
||||
def digits(self) -> Iterable[int]:
|
||||
for x in str(self):
|
||||
yield int(x)
|
||||
|
||||
def digits_sum(self) -> int:
|
||||
return sum(int(x) for x in str(self))
|
||||
|
||||
|
||||
class String(str):
|
||||
def swap(self, x: int, y: int) -> String:
|
||||
x, y = min(x, y), max(x, y)
|
||||
return String(self[:x] + self[y] + self[x + 1 : y] + self[x] + self[y + 1 :])
|
||||
|
||||
def rotate(self, n: int) -> String:
|
||||
if n == 0:
|
||||
return self
|
||||
while n < 0:
|
||||
n += len(self)
|
||||
while n > len(self):
|
||||
n -= len(self)
|
||||
return String(self[-n:] + self[: len(self) - n])
|
||||
|
||||
def reverse(self, start: int = 0, end: int = None) -> String:
|
||||
if end is None:
|
||||
end = len(self) - 1
|
||||
return String(self[:start] + "".join(reversed(self[start : end + 1])) + self[end + 1 :])
|
||||
|
||||
def __getitem__(self, item) -> String:
|
||||
return String(super().__getitem__(item))
|
||||
|
||||
def __floordiv__(self, other: int) -> list[String]:
|
||||
d = ceil(len(self) / other)
|
||||
return [self[i * d:i * d + d] for i in range(other)]
|
||||
|
||||
|
||||
def __truediv__(self, other: int) -> list[String]:
|
||||
if len(self) % other != 0:
|
||||
raise ValueError("String length is Not a multiple of {}".format(other))
|
||||
|
||||
d = len(self) // other
|
||||
return [self[i * d:i * d + d] for i in range(other)]
|
||||
|
||||
|
||||
|
||||
class List(list):
|
||||
def moving_window(self, size: int = 2) -> Iterable[List]:
|
||||
if len(self) % size != 0:
|
||||
raise ValueError("number of list items must be divisible by size")
|
||||
|
||||
for i in range(len(self) // size):
|
||||
this_window = List()
|
||||
for j in range(size):
|
||||
this_window.append(self[i * size + j])
|
||||
|
||||
yield this_window
|
||||
|
||||
|
||||
class Range:
|
||||
def __init__(self, start: int, end: int) -> None:
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def overlaps(self, other: Range) -> bool:
|
||||
return other.start <= self.start <= other.end or other.start <= self.end <= other.end or (other.start <= self.start and other.end >= self.end) or (self.start <= other.start and self.end >= other.end)
|
||||
|
||||
def merge(self, other: Range) -> Range:
|
||||
if not self.overlaps(other):
|
||||
raise ValueError("Ranges do not overlap")
|
||||
|
||||
return Range(min(self.start, other.start), max(self.end, other.end))
|
||||
|
||||
def __contains__(self, item: int) -> bool:
|
||||
return self.start <= item <= self.end
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.end - self.start + 1
|
||||
|
||||
def __iter__(self) -> Iterable[int]:
|
||||
for x in range(self.start, self.end + 1):
|
||||
yield x
|
||||
|
||||
def __reversed__(self) -> Iterable[int]:
|
||||
for x in range(self.end, self.start - 1, -1):
|
||||
yield x
|
||||
@ -1,657 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from .coordinate import Line, Coordinate, Rectangle
|
||||
from enum import Enum
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
class Color(str, Enum):
|
||||
ALICEBLUE = "#F0F8FF"
|
||||
ANTIQUEWHITE = "#FAEBD7"
|
||||
ANTIQUEWHITE1 = "#FFEFDB"
|
||||
ANTIQUEWHITE2 = "#EEDFCC"
|
||||
ANTIQUEWHITE3 = "#CDC0B0"
|
||||
ANTIQUEWHITE4 = "#8B8378"
|
||||
AQUA = "#00FFFF"
|
||||
AQUAMARINE1 = "#7FFFD4"
|
||||
AQUAMARINE2 = "#76EEC6"
|
||||
AQUAMARINE3 = "#66CDAA"
|
||||
AQUAMARINE4 = "#458B74"
|
||||
AZURE = "#007FFF"
|
||||
AZURE1 = "#F0FFFF"
|
||||
AZURE2 = "#E0EEEE"
|
||||
AZURE3 = "#C1CDCD"
|
||||
AZURE4 = "#838B8B"
|
||||
BANANA = "#E3CF57"
|
||||
BEIGE = "#F5F5DC"
|
||||
BISQUE1 = "#FFE4C4"
|
||||
BISQUE2 = "#EED5B7"
|
||||
BISQUE3 = "#CDB79E"
|
||||
BISQUE4 = "#8B7D6B"
|
||||
BLACK = "#000000"
|
||||
BLANCHEDALMOND = "#FFEBCD"
|
||||
BLUE = "#0000FF"
|
||||
BLUE2 = "#0000EE"
|
||||
BLUE3 = "#0000CD"
|
||||
BLUE4 = "#00008B"
|
||||
BLUEVIOLET = "#8A2BE2"
|
||||
BRICK = "#9C661F"
|
||||
BROWN = "#A52A2A"
|
||||
BROWN1 = "#FF4040"
|
||||
BROWN2 = "#EE3B3B"
|
||||
BROWN3 = "#CD3333"
|
||||
BROWN4 = "#8B2323"
|
||||
BURLYWOOD = "#DEB887"
|
||||
BURLYWOOD1 = "#FFD39B"
|
||||
BURLYWOOD2 = "#EEC591"
|
||||
BURLYWOOD3 = "#CDAA7D"
|
||||
BURLYWOOD4 = "#8B7355"
|
||||
BURNTSIENNA = "#8A360F"
|
||||
BURNTUMBER = "#8A3324"
|
||||
CADETBLUE = "#5F9EA0"
|
||||
CADETBLUE1 = "#98F5FF"
|
||||
CADETBLUE2 = "#8EE5EE"
|
||||
CADETBLUE3 = "#7AC5CD"
|
||||
CADETBLUE4 = "#53868B"
|
||||
CADMIUMORANGE = "#FF6103"
|
||||
CADMIUMYELLOW = "#FF9912"
|
||||
CARROT = "#ED9121"
|
||||
CHARTREUSE = "#DFFF00"
|
||||
CHARTREUSE1 = "#7FFF00"
|
||||
CHARTREUSE2 = "#76EE00"
|
||||
CHARTREUSE3 = "#66CD00"
|
||||
CHARTREUSE4 = "#458B00"
|
||||
CHOCOLATE = "#D2691E"
|
||||
CHOCOLATE1 = "#FF7F24"
|
||||
CHOCOLATE2 = "#EE7621"
|
||||
CHOCOLATE3 = "#CD661D"
|
||||
CHOCOLATE4 = "#8B4513"
|
||||
COBALT = "#3D59AB"
|
||||
COBALTGREEN = "#3D9140"
|
||||
COLDGREY = "#808A87"
|
||||
CORAL = "#FF7F50"
|
||||
CORAL1 = "#FF7256"
|
||||
CORAL2 = "#EE6A50"
|
||||
CORAL3 = "#CD5B45"
|
||||
CORAL4 = "#8B3E2F"
|
||||
CORNFLOWERBLUE = "#6495ED"
|
||||
CORNSILK = "#FFF8DC"
|
||||
CORNSILK1 = "#FFF8DC"
|
||||
CORNSILK2 = "#EEE8CD"
|
||||
CORNSILK3 = "#CDC8B1"
|
||||
CORNSILK4 = "#8B8878"
|
||||
CRIMSON = "#DC143C"
|
||||
CYAN = "#00FFFF"
|
||||
CYAN2 = "#00EEEE"
|
||||
CYAN3 = "#00CDCD"
|
||||
CYAN4 = "#008B8B"
|
||||
DARKGOLDENROD = "#B8860B"
|
||||
DARKGOLDENROD1 = "#FFB90F"
|
||||
DARKGOLDENROD2 = "#EEAD0E"
|
||||
DARKGOLDENROD3 = "#CD950C"
|
||||
DARKGOLDENROD4 = "#8B6508"
|
||||
DARKGRAY = "#A9A9A9"
|
||||
DARKGREEN = "#006400"
|
||||
DARKKHAKI = "#BDB76B"
|
||||
DARKOLIVEGREEN = "#556B2F"
|
||||
DARKOLIVEGREEN1 = "#CAFF70"
|
||||
DARKOLIVEGREEN2 = "#BCEE68"
|
||||
DARKOLIVEGREEN3 = "#A2CD5A"
|
||||
DARKOLIVEGREEN4 = "#6E8B3D"
|
||||
DARKORANGE = "#FF8C00"
|
||||
DARKORANGE1 = "#FF7F00"
|
||||
DARKORANGE2 = "#EE7600"
|
||||
DARKORANGE3 = "#CD6600"
|
||||
DARKORANGE4 = "#8B4500"
|
||||
DARKORCHID = "#9932CC"
|
||||
DARKORCHID1 = "#BF3EFF"
|
||||
DARKORCHID2 = "#B23AEE"
|
||||
DARKORCHID3 = "#9A32CD"
|
||||
DARKORCHID4 = "#68228B"
|
||||
DARKSALMON = "#E9967A"
|
||||
DARKSEAGREEN = "#8FBC8F"
|
||||
DARKSEAGREEN1 = "#C1FFC1"
|
||||
DARKSEAGREEN2 = "#B4EEB4"
|
||||
DARKSEAGREEN3 = "#9BCD9B"
|
||||
DARKSEAGREEN4 = "#698B69"
|
||||
DARKSLATEBLUE = "#483D8B"
|
||||
DARKSLATEGRAY = "#2F4F4F"
|
||||
DARKSLATEGRAY1 = "#97FFFF"
|
||||
DARKSLATEGRAY2 = "#8DEEEE"
|
||||
DARKSLATEGRAY3 = "#79CDCD"
|
||||
DARKSLATEGRAY4 = "#528B8B"
|
||||
DARKTURQUOISE = "#00CED1"
|
||||
DARKVIOLET = "#9400D3"
|
||||
DEEPPINK1 = "#FF1493"
|
||||
DEEPPINK2 = "#EE1289"
|
||||
DEEPPINK3 = "#CD1076"
|
||||
DEEPPINK4 = "#8B0A50"
|
||||
DEEPSKYBLUE = "#00BFFF"
|
||||
DEEPSKYBLUE1 = "#00BFFF"
|
||||
DEEPSKYBLUE2 = "#00B2EE"
|
||||
DEEPSKYBLUE3 = "#009ACD"
|
||||
DEEPSKYBLUE4 = "#00688B"
|
||||
DIMGRAY = "#696969"
|
||||
DODGERBLUE1 = "#1E90FF"
|
||||
DODGERBLUE2 = "#1C86EE"
|
||||
DODGERBLUE3 = "#1874CD"
|
||||
DODGERBLUE4 = "#104E8B"
|
||||
EGGSHELL = "#FCE6C9"
|
||||
EMERALDGREEN = "#00C957"
|
||||
FIREBRICK = "#B22222"
|
||||
FIREBRICK1 = "#FF3030"
|
||||
FIREBRICK2 = "#EE2C2C"
|
||||
FIREBRICK3 = "#CD2626"
|
||||
FIREBRICK4 = "#8B1A1A"
|
||||
FLESH = "#FF7D40"
|
||||
FLORALWHITE = "#FFFAF0"
|
||||
FORESTGREEN = "#228B22"
|
||||
GAINSBORO = "#DCDCDC"
|
||||
GHOSTWHITE = "#F8F8FF"
|
||||
GOLD1 = "#FFD700"
|
||||
GOLD2 = "#EEC900"
|
||||
GOLD3 = "#CDAD00"
|
||||
GOLD4 = "#8B7500"
|
||||
GOLDENROD = "#DAA520"
|
||||
GOLDENROD1 = "#FFC125"
|
||||
GOLDENROD2 = "#EEB422"
|
||||
GOLDENROD3 = "#CD9B1D"
|
||||
GOLDENROD4 = "#8B6914"
|
||||
GRAY = "#808080"
|
||||
GRAY1 = "#030303"
|
||||
GRAY10 = "#1A1A1A"
|
||||
GRAY11 = "#1C1C1C"
|
||||
GRAY12 = "#1F1F1F"
|
||||
GRAY13 = "#212121"
|
||||
GRAY14 = "#242424"
|
||||
GRAY15 = "#262626"
|
||||
GRAY16 = "#292929"
|
||||
GRAY17 = "#2B2B2B"
|
||||
GRAY18 = "#2E2E2E"
|
||||
GRAY19 = "#303030"
|
||||
GRAY2 = "#050505"
|
||||
GRAY20 = "#333333"
|
||||
GRAY21 = "#363636"
|
||||
GRAY22 = "#383838"
|
||||
GRAY23 = "#3B3B3B"
|
||||
GRAY24 = "#3D3D3D"
|
||||
GRAY25 = "#404040"
|
||||
GRAY26 = "#424242"
|
||||
GRAY27 = "#454545"
|
||||
GRAY28 = "#474747"
|
||||
GRAY29 = "#4A4A4A"
|
||||
GRAY3 = "#080808"
|
||||
GRAY30 = "#4D4D4D"
|
||||
GRAY31 = "#4F4F4F"
|
||||
GRAY32 = "#525252"
|
||||
GRAY33 = "#545454"
|
||||
GRAY34 = "#575757"
|
||||
GRAY35 = "#595959"
|
||||
GRAY36 = "#5C5C5C"
|
||||
GRAY37 = "#5E5E5E"
|
||||
GRAY38 = "#616161"
|
||||
GRAY39 = "#636363"
|
||||
GRAY4 = "#0A0A0A"
|
||||
GRAY40 = "#666666"
|
||||
GRAY42 = "#6B6B6B"
|
||||
GRAY43 = "#6E6E6E"
|
||||
GRAY44 = "#707070"
|
||||
GRAY45 = "#737373"
|
||||
GRAY46 = "#757575"
|
||||
GRAY47 = "#787878"
|
||||
GRAY48 = "#7A7A7A"
|
||||
GRAY49 = "#7D7D7D"
|
||||
GRAY5 = "#0D0D0D"
|
||||
GRAY50 = "#7F7F7F"
|
||||
GRAY51 = "#828282"
|
||||
GRAY52 = "#858585"
|
||||
GRAY53 = "#878787"
|
||||
GRAY54 = "#8A8A8A"
|
||||
GRAY55 = "#8C8C8C"
|
||||
GRAY56 = "#8F8F8F"
|
||||
GRAY57 = "#919191"
|
||||
GRAY58 = "#949494"
|
||||
GRAY59 = "#969696"
|
||||
GRAY6 = "#0F0F0F"
|
||||
GRAY60 = "#999999"
|
||||
GRAY61 = "#9C9C9C"
|
||||
GRAY62 = "#9E9E9E"
|
||||
GRAY63 = "#A1A1A1"
|
||||
GRAY64 = "#A3A3A3"
|
||||
GRAY65 = "#A6A6A6"
|
||||
GRAY66 = "#A8A8A8"
|
||||
GRAY67 = "#ABABAB"
|
||||
GRAY68 = "#ADADAD"
|
||||
GRAY69 = "#B0B0B0"
|
||||
GRAY7 = "#121212"
|
||||
GRAY70 = "#B3B3B3"
|
||||
GRAY71 = "#B5B5B5"
|
||||
GRAY72 = "#B8B8B8"
|
||||
GRAY73 = "#BABABA"
|
||||
GRAY74 = "#BDBDBD"
|
||||
GRAY75 = "#BFBFBF"
|
||||
GRAY76 = "#C2C2C2"
|
||||
GRAY77 = "#C4C4C4"
|
||||
GRAY78 = "#C7C7C7"
|
||||
GRAY79 = "#C9C9C9"
|
||||
GRAY8 = "#141414"
|
||||
GRAY80 = "#CCCCCC"
|
||||
GRAY81 = "#CFCFCF"
|
||||
GRAY82 = "#D1D1D1"
|
||||
GRAY83 = "#D4D4D4"
|
||||
GRAY84 = "#D6D6D6"
|
||||
GRAY85 = "#D9D9D9"
|
||||
GRAY86 = "#DBDBDB"
|
||||
GRAY87 = "#DEDEDE"
|
||||
GRAY88 = "#E0E0E0"
|
||||
GRAY89 = "#E3E3E3"
|
||||
GRAY9 = "#171717"
|
||||
GRAY90 = "#E5E5E5"
|
||||
GRAY91 = "#E8E8E8"
|
||||
GRAY92 = "#EBEBEB"
|
||||
GRAY93 = "#EDEDED"
|
||||
GRAY94 = "#F0F0F0"
|
||||
GRAY95 = "#F2F2F2"
|
||||
GRAY97 = "#F7F7F7"
|
||||
GRAY98 = "#FAFAFA"
|
||||
GRAY99 = "#FCFCFC"
|
||||
GREEN = "#008000"
|
||||
GREEN1 = "#00FF00"
|
||||
GREEN2 = "#00EE00"
|
||||
GREEN3 = "#00CD00"
|
||||
GREEN4 = "#008B00"
|
||||
GREENYELLOW = "#ADFF2F"
|
||||
HONEYDEW1 = "#F0FFF0"
|
||||
HONEYDEW2 = "#E0EEE0"
|
||||
HONEYDEW3 = "#C1CDC1"
|
||||
HONEYDEW4 = "#838B83"
|
||||
HOTPINK = "#FF69B4"
|
||||
HOTPINK1 = "#FF6EB4"
|
||||
HOTPINK2 = "#EE6AA7"
|
||||
HOTPINK3 = "#CD6090"
|
||||
HOTPINK4 = "#8B3A62"
|
||||
INDIANRED = "#CD5C5C"
|
||||
INDIANRED1 = "#FF6A6A"
|
||||
INDIANRED2 = "#EE6363"
|
||||
INDIANRED3 = "#CD5555"
|
||||
INDIANRED4 = "#8B3A3A"
|
||||
INDIGO = "#4B0082"
|
||||
IVORY1 = "#FFFFF0"
|
||||
IVORY2 = "#EEEEE0"
|
||||
IVORY3 = "#CDCDC1"
|
||||
IVORY4 = "#8B8B83"
|
||||
IVORYBLACK = "#292421"
|
||||
KHAKI = "#F0E68C"
|
||||
KHAKI1 = "#FFF68F"
|
||||
KHAKI2 = "#EEE685"
|
||||
KHAKI3 = "#CDC673"
|
||||
KHAKI4 = "#8B864E"
|
||||
LAVENDER = "#E6E6FA"
|
||||
LAVENDERBLUSH1 = "#FFF0F5"
|
||||
LAVENDERBLUSH2 = "#EEE0E5"
|
||||
LAVENDERBLUSH3 = "#CDC1C5"
|
||||
LAVENDERBLUSH4 = "#8B8386"
|
||||
LAWNGREEN = "#7CFC00"
|
||||
LEMONCHIFFON1 = "#FFFACD"
|
||||
LEMONCHIFFON2 = "#EEE9BF"
|
||||
LEMONCHIFFON3 = "#CDC9A5"
|
||||
LEMONCHIFFON4 = "#8B8970"
|
||||
LIGHTBLUE = "#ADD8E6"
|
||||
LIGHTBLUE1 = "#BFEFFF"
|
||||
LIGHTBLUE2 = "#B2DFEE"
|
||||
LIGHTBLUE3 = "#9AC0CD"
|
||||
LIGHTBLUE4 = "#68838B"
|
||||
LIGHTCORAL = "#F08080"
|
||||
LIGHTCYAN1 = "#E0FFFF"
|
||||
LIGHTCYAN2 = "#D1EEEE"
|
||||
LIGHTCYAN3 = "#B4CDCD"
|
||||
LIGHTCYAN4 = "#7A8B8B"
|
||||
LIGHTGOLDENROD1 = "#FFEC8B"
|
||||
LIGHTGOLDENROD2 = "#EEDC82"
|
||||
LIGHTGOLDENROD3 = "#CDBE70"
|
||||
LIGHTGOLDENROD4 = "#8B814C"
|
||||
LIGHTGOLDENRODYELLOW = "#FAFAD2"
|
||||
LIGHTGREY = "#D3D3D3"
|
||||
LIGHTPINK = "#FFB6C1"
|
||||
LIGHTPINK1 = "#FFAEB9"
|
||||
LIGHTPINK2 = "#EEA2AD"
|
||||
LIGHTPINK3 = "#CD8C95"
|
||||
LIGHTPINK4 = "#8B5F65"
|
||||
LIGHTSALMON1 = "#FFA07A"
|
||||
LIGHTSALMON2 = "#EE9572"
|
||||
LIGHTSALMON3 = "#CD8162"
|
||||
LIGHTSALMON4 = "#8B5742"
|
||||
LIGHTSEAGREEN = "#20B2AA"
|
||||
LIGHTSKYBLUE = "#87CEFA"
|
||||
LIGHTSKYBLUE1 = "#B0E2FF"
|
||||
LIGHTSKYBLUE2 = "#A4D3EE"
|
||||
LIGHTSKYBLUE3 = "#8DB6CD"
|
||||
LIGHTSKYBLUE4 = "#607B8B"
|
||||
LIGHTSLATEBLUE = "#8470FF"
|
||||
LIGHTSLATEGRAY = "#778899"
|
||||
LIGHTSTEELBLUE = "#B0C4DE"
|
||||
LIGHTSTEELBLUE1 = "#CAE1FF"
|
||||
LIGHTSTEELBLUE2 = "#BCD2EE"
|
||||
LIGHTSTEELBLUE3 = "#A2B5CD"
|
||||
LIGHTSTEELBLUE4 = "#6E7B8B"
|
||||
LIGHTYELLOW1 = "#FFFFE0"
|
||||
LIGHTYELLOW2 = "#EEEED1"
|
||||
LIGHTYELLOW3 = "#CDCDB4"
|
||||
LIGHTYELLOW4 = "#8B8B7A"
|
||||
LIMEGREEN = "#32CD32"
|
||||
LINEN = "#FAF0E6"
|
||||
MAGENTA = "#FF00FF"
|
||||
MAGENTA2 = "#EE00EE"
|
||||
MAGENTA3 = "#CD00CD"
|
||||
MAGENTA4 = "#8B008B"
|
||||
MANGANESEBLUE = "#03A89E"
|
||||
MAROON = "#800000"
|
||||
MAROON1 = "#FF34B3"
|
||||
MAROON2 = "#EE30A7"
|
||||
MAROON3 = "#CD2990"
|
||||
MAROON4 = "#8B1C62"
|
||||
MEDIUMORCHID = "#BA55D3"
|
||||
MEDIUMORCHID1 = "#E066FF"
|
||||
MEDIUMORCHID2 = "#D15FEE"
|
||||
MEDIUMORCHID3 = "#B452CD"
|
||||
MEDIUMORCHID4 = "#7A378B"
|
||||
MEDIUMPURPLE = "#9370DB"
|
||||
MEDIUMPURPLE1 = "#AB82FF"
|
||||
MEDIUMPURPLE2 = "#9F79EE"
|
||||
MEDIUMPURPLE3 = "#8968CD"
|
||||
MEDIUMPURPLE4 = "#5D478B"
|
||||
MEDIUMSEAGREEN = "#3CB371"
|
||||
MEDIUMSLATEBLUE = "#7B68EE"
|
||||
MEDIUMSPRINGGREEN = "#00FA9A"
|
||||
MEDIUMTURQUOISE = "#48D1CC"
|
||||
MEDIUMVIOLETRED = "#C71585"
|
||||
MELON = "#E3A869"
|
||||
MIDNIGHTBLUE = "#191970"
|
||||
MINT = "#BDFCC9"
|
||||
MINTCREAM = "#F5FFFA"
|
||||
MISTYROSE1 = "#FFE4E1"
|
||||
MISTYROSE2 = "#EED5D2"
|
||||
MISTYROSE3 = "#CDB7B5"
|
||||
MISTYROSE4 = "#8B7D7B"
|
||||
MOCCASIN = "#FFE4B5"
|
||||
NAVAJOWHITE1 = "#FFDEAD"
|
||||
NAVAJOWHITE2 = "#EECFA1"
|
||||
NAVAJOWHITE3 = "#CDB38B"
|
||||
NAVAJOWHITE4 = "#8B795E"
|
||||
NAVY = "#000080"
|
||||
OLDLACE = "#FDF5E6"
|
||||
OLIVE = "#808000"
|
||||
OLIVEDRAB = "#6B8E23"
|
||||
OLIVEDRAB1 = "#C0FF3E"
|
||||
OLIVEDRAB2 = "#B3EE3A"
|
||||
OLIVEDRAB3 = "#9ACD32"
|
||||
OLIVEDRAB4 = "#698B22"
|
||||
ORANGE = "#FF8000"
|
||||
ORANGE1 = "#FFA500"
|
||||
ORANGE2 = "#EE9A00"
|
||||
ORANGE3 = "#CD8500"
|
||||
ORANGE4 = "#8B5A00"
|
||||
ORANGERED1 = "#FF4500"
|
||||
ORANGERED2 = "#EE4000"
|
||||
ORANGERED3 = "#CD3700"
|
||||
ORANGERED4 = "#8B2500"
|
||||
ORCHID = "#DA70D6"
|
||||
ORCHID1 = "#FF83FA"
|
||||
ORCHID2 = "#EE7AE9"
|
||||
ORCHID3 = "#CD69C9"
|
||||
ORCHID4 = "#8B4789"
|
||||
PALEGOLDENROD = "#EEE8AA"
|
||||
PALEGREEN = "#98FB98"
|
||||
PALEGREEN1 = "#9AFF9A"
|
||||
PALEGREEN2 = "#90EE90"
|
||||
PALEGREEN3 = "#7CCD7C"
|
||||
PALEGREEN4 = "#548B54"
|
||||
PALETURQUOISE1 = "#BBFFFF"
|
||||
PALETURQUOISE2 = "#AEEEEE"
|
||||
PALETURQUOISE3 = "#96CDCD"
|
||||
PALETURQUOISE4 = "#668B8B"
|
||||
PALEVIOLETRED = "#DB7093"
|
||||
PALEVIOLETRED1 = "#FF82AB"
|
||||
PALEVIOLETRED2 = "#EE799F"
|
||||
PALEVIOLETRED3 = "#CD6889"
|
||||
PALEVIOLETRED4 = "#8B475D"
|
||||
PAPAYAWHIP = "#FFEFD5"
|
||||
PEACHPUFF1 = "#FFDAB9"
|
||||
PEACHPUFF2 = "#EECBAD"
|
||||
PEACHPUFF3 = "#CDAF95"
|
||||
PEACHPUFF4 = "#8B7765"
|
||||
PEACOCK = "#33A1C9"
|
||||
PINK = "#FFC0CB"
|
||||
PINK1 = "#FFB5C5"
|
||||
PINK2 = "#EEA9B8"
|
||||
PINK3 = "#CD919E"
|
||||
PINK4 = "#8B636C"
|
||||
PLUM = "#DDA0DD"
|
||||
PLUM1 = "#FFBBFF"
|
||||
PLUM2 = "#EEAEEE"
|
||||
PLUM3 = "#CD96CD"
|
||||
PLUM4 = "#8B668B"
|
||||
POWDERBLUE = "#B0E0E6"
|
||||
PURPLE = "#800080"
|
||||
PURPLE1 = "#9B30FF"
|
||||
PURPLE2 = "#912CEE"
|
||||
PURPLE3 = "#7D26CD"
|
||||
PURPLE4 = "#551A8B"
|
||||
RASPBERRY = "#872657"
|
||||
RAWSIENNA = "#C76114"
|
||||
RED1 = "#FF0000"
|
||||
RED2 = "#EE0000"
|
||||
RED3 = "#CD0000"
|
||||
RED4 = "#8B0000"
|
||||
ROSYBROWN = "#BC8F8F"
|
||||
ROSYBROWN1 = "#FFC1C1"
|
||||
ROSYBROWN2 = "#EEB4B4"
|
||||
ROSYBROWN3 = "#CD9B9B"
|
||||
ROSYBROWN4 = "#8B6969"
|
||||
ROYALBLUE = "#4169E1"
|
||||
ROYALBLUE1 = "#4876FF"
|
||||
ROYALBLUE2 = "#436EEE"
|
||||
ROYALBLUE3 = "#3A5FCD"
|
||||
ROYALBLUE4 = "#27408B"
|
||||
SALMON = "#FA8072"
|
||||
SALMON1 = "#FF8C69"
|
||||
SALMON2 = "#EE8262"
|
||||
SALMON3 = "#CD7054"
|
||||
SALMON4 = "#8B4C39"
|
||||
SANDYBROWN = "#F4A460"
|
||||
SAPGREEN = "#308014"
|
||||
SEAGREEN1 = "#54FF9F"
|
||||
SEAGREEN2 = "#4EEE94"
|
||||
SEAGREEN3 = "#43CD80"
|
||||
SEAGREEN4 = "#2E8B57"
|
||||
SEASHELL1 = "#FFF5EE"
|
||||
SEASHELL2 = "#EEE5DE"
|
||||
SEASHELL3 = "#CDC5BF"
|
||||
SEASHELL4 = "#8B8682"
|
||||
SEPIA = "#5E2612"
|
||||
SGIBEET = "#8E388E"
|
||||
SGIBRIGHTGRAY = "#C5C1AA"
|
||||
SGICHARTREUSE = "#71C671"
|
||||
SGIDARKGRAY = "#555555"
|
||||
SGIGRAY12 = "#1E1E1E"
|
||||
SGIGRAY16 = "#282828"
|
||||
SGIGRAY32 = "#515151"
|
||||
SGIGRAY36 = "#5B5B5B"
|
||||
SGIGRAY52 = "#848484"
|
||||
SGIGRAY56 = "#8E8E8E"
|
||||
SGIGRAY72 = "#B7B7B7"
|
||||
SGIGRAY76 = "#C1C1C1"
|
||||
SGIGRAY92 = "#EAEAEA"
|
||||
SGIGRAY96 = "#F4F4F4"
|
||||
SGILIGHTBLUE = "#7D9EC0"
|
||||
SGILIGHTGRAY = "#AAAAAA"
|
||||
SGIOLIVEDRAB = "#8E8E38"
|
||||
SGISALMON = "#C67171"
|
||||
SGISLATEBLUE = "#7171C6"
|
||||
SGITEAL = "#388E8E"
|
||||
SIENNA = "#A0522D"
|
||||
SIENNA1 = "#FF8247"
|
||||
SIENNA2 = "#EE7942"
|
||||
SIENNA3 = "#CD6839"
|
||||
SIENNA4 = "#8B4726"
|
||||
SILVER = "#C0C0C0"
|
||||
SKYBLUE = "#87CEEB"
|
||||
SKYBLUE1 = "#87CEFF"
|
||||
SKYBLUE2 = "#7EC0EE"
|
||||
SKYBLUE3 = "#6CA6CD"
|
||||
SKYBLUE4 = "#4A708B"
|
||||
SLATEBLUE = "#6A5ACD"
|
||||
SLATEBLUE1 = "#836FFF"
|
||||
SLATEBLUE2 = "#7A67EE"
|
||||
SLATEBLUE3 = "#6959CD"
|
||||
SLATEBLUE4 = "#473C8B"
|
||||
SLATEGRAY = "#708090"
|
||||
SLATEGRAY1 = "#C6E2FF"
|
||||
SLATEGRAY2 = "#B9D3EE"
|
||||
SLATEGRAY3 = "#9FB6CD"
|
||||
SLATEGRAY4 = "#6C7B8B"
|
||||
SNOW1 = "#FFFAFA"
|
||||
SNOW2 = "#EEE9E9"
|
||||
SNOW3 = "#CDC9C9"
|
||||
SNOW4 = "#8B8989"
|
||||
SPRINGGREEN = "#00FF7F"
|
||||
SPRINGGREEN1 = "#00EE76"
|
||||
SPRINGGREEN2 = "#00CD66"
|
||||
SPRINGGREEN3 = "#008B45"
|
||||
STEELBLUE = "#4682B4"
|
||||
STEELBLUE1 = "#63B8FF"
|
||||
STEELBLUE2 = "#5CACEE"
|
||||
STEELBLUE3 = "#4F94CD"
|
||||
STEELBLUE4 = "#36648B"
|
||||
TAN = "#D2B48C"
|
||||
TAN1 = "#FFA54F"
|
||||
TAN2 = "#EE9A49"
|
||||
TAN3 = "#CD853F"
|
||||
TAN4 = "#8B5A2B"
|
||||
TEAL = "#008080"
|
||||
THISTLE = "#D8BFD8"
|
||||
THISTLE1 = "#FFE1FF"
|
||||
THISTLE2 = "#EED2EE"
|
||||
THISTLE3 = "#CDB5CD"
|
||||
THISTLE4 = "#8B7B8B"
|
||||
TOMATO1 = "#FF6347"
|
||||
TOMATO2 = "#EE5C42"
|
||||
TOMATO3 = "#CD4F39"
|
||||
TOMATO4 = "#8B3626"
|
||||
TURQUOISE = "#40E0D0"
|
||||
TURQUOISE1 = "#00F5FF"
|
||||
TURQUOISE2 = "#00E5EE"
|
||||
TURQUOISE3 = "#00C5CD"
|
||||
TURQUOISE4 = "#00868B"
|
||||
TURQUOISEBLUE = "#00C78C"
|
||||
VIOLET = "#EE82EE"
|
||||
VIOLETRED = "#D02090"
|
||||
VIOLETRED1 = "#FF3E96"
|
||||
VIOLETRED2 = "#EE3A8C"
|
||||
VIOLETRED3 = "#CD3278"
|
||||
VIOLETRED4 = "#8B2252"
|
||||
WARMGREY = "#808069"
|
||||
WHEAT = "#F5DEB3"
|
||||
WHEAT1 = "#FFE7BA"
|
||||
WHEAT2 = "#EED8AE"
|
||||
WHEAT3 = "#CDBA96"
|
||||
WHEAT4 = "#8B7E66"
|
||||
WHITE = "#FFFFFF"
|
||||
WHITESMOKE = "#F5F5F5"
|
||||
YELLOW1 = "#FFFF00"
|
||||
YELLOW2 = "#EEEE00"
|
||||
YELLOW3 = "#CDCD00"
|
||||
YELLOW4 = "#8B8B00"
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self, width: int = 1280, height: int = 1024, bg_color: str = Color.BLACK, fg_color: str = Color.WHITE):
|
||||
self.width, self.height = width, height
|
||||
self.bg_color, self.fg_color = bg_color, fg_color
|
||||
|
||||
self.__tk_root = tk.Tk()
|
||||
self.__canvas = tk.Canvas(master=self.__tk_root, width=width, height=height, bg=bg_color)
|
||||
self.__canvas.pack(fill=tk.BOTH, expand=tk.YES)
|
||||
self.__canvas.bind("<MouseWheel>", self._zoom)
|
||||
self.__canvas.bind("<ButtonPress-1>", self._scroll_start)
|
||||
self.__canvas.bind("<B1-Motion>", self._scroll_move)
|
||||
|
||||
self.__boundary_box = [None, None, None, None]
|
||||
|
||||
def _update_boundaries(self, coord: Coordinate | tuple[int, int]) -> None:
|
||||
if self.__boundary_box[0] is None or coord[0] < self.__boundary_box[0]:
|
||||
self.__boundary_box[0] = coord[0]
|
||||
if self.__boundary_box[2] is None or coord[0] > self.__boundary_box[2]:
|
||||
self.__boundary_box[2] = coord[0]
|
||||
if self.__boundary_box[1] is None or coord[1] < self.__boundary_box[1]:
|
||||
self.__boundary_box[1] = coord[1]
|
||||
if self.__boundary_box[3] is None or coord[1] > self.__boundary_box[3]:
|
||||
self.__boundary_box[3] = coord[1]
|
||||
|
||||
def _zoom(self, event: tk.Event) -> None:
|
||||
amount = 0.95 if event.delta < 0 else 1.05
|
||||
self.__canvas.scale(tk.ALL, 0, 0, amount, amount)
|
||||
|
||||
def _scroll_start(self, event: tk.Event) -> None:
|
||||
self.__canvas.scan_mark(event.x, event.y)
|
||||
|
||||
def _scroll_move(self, event: tk.Event) -> None:
|
||||
self.__canvas.scan_dragto(event.x, event.y, gain=1)
|
||||
|
||||
def clear(self, keep_boundaries: bool = False) -> None:
|
||||
self.__canvas.delete(tk.ALL)
|
||||
if not keep_boundaries:
|
||||
self.__boundary_box = [None, None, None, None]
|
||||
|
||||
def draw_point(self, coord: Coordinate, size: int = 5, color: str = None):
|
||||
if color is None:
|
||||
color = self.fg_color
|
||||
|
||||
offset = size / 2
|
||||
|
||||
self.__canvas.create_oval(coord.x - offset, coord.y - offset, coord.x + offset, coord.y + offset, fill=color)
|
||||
self._update_boundaries(coord)
|
||||
|
||||
def draw_line(self, line: Line, thickness: int = 1, color: str = None) -> None:
|
||||
if color is None:
|
||||
color = self.fg_color
|
||||
|
||||
self.__canvas.create_line(line.start.x, line.start.y, line.end.x, line.end.y, fill=color, width=thickness)
|
||||
self._update_boundaries(line.start)
|
||||
self._update_boundaries(line.end)
|
||||
|
||||
def draw_rectangle(self, rectangle: Rectangle, thickness: int = 1, color: str = None, fill: bool = False) -> None:
|
||||
if color is None:
|
||||
color = self.fg_color
|
||||
|
||||
if fill:
|
||||
fill = color
|
||||
else:
|
||||
fill = self.bg_color
|
||||
|
||||
self.__canvas.create_rectangle(
|
||||
rectangle.top_left.x,
|
||||
rectangle.top_left.y,
|
||||
rectangle.bottom_right.x,
|
||||
rectangle.bottom_right.y,
|
||||
fill=fill,
|
||||
outline=color,
|
||||
width=thickness,
|
||||
)
|
||||
self._update_boundaries(rectangle.top_left)
|
||||
self._update_boundaries(rectangle.bottom_right)
|
||||
|
||||
def realign(self, padding: int = 1) -> None:
|
||||
if self.__boundary_box[0] is not None and self.__boundary_box[0] < 0:
|
||||
self.__canvas.move(tk.ALL, abs(self.__boundary_box[0]) + padding, 0)
|
||||
if self.__boundary_box[1] is not None and self.__boundary_box[1] < 0:
|
||||
self.__canvas.move(tk.ALL, 0, abs(self.__boundary_box[1]) + padding)
|
||||
dim_x = self.__boundary_box[2] - self.__boundary_box[0] + 2 * padding
|
||||
dim_y = self.__boundary_box[3] - self.__boundary_box[1] + 2 * padding
|
||||
scale = min(self.width / dim_x, self.height / dim_y)
|
||||
self.__canvas.scale(tk.ALL, 0, 0, scale, scale)
|
||||
self.__canvas.update()
|
||||
|
||||
def done(self, realign: bool = True) -> None:
|
||||
if realign:
|
||||
self.realign()
|
||||
self.__canvas.mainloop()
|
||||
Loading…
Reference in New Issue
Block a user