import datetime
import os
import math
from RWESharp.info import PATH_FILES_CACHE, _LOG
from PySide6.QtGui import QColor, QIcon, QPixmap
from PySide6.QtCore import QPoint, QPointF, QFile, QByteArray, QRect, QLineF, QRectF
from collections.abc import Callable
[docs]
def log(message, error=False) -> None:
"""Logs a message in both terminal and log file
:param message: message to log
:param error: Whether it's error or not
:return: None
"""
s = f"[{datetime.datetime.now().strftime('%H:%M')}; {'ERROR' if error else ' INFO'}]: {message}\n"
print(s, end="", file=_LOG, flush=True)
print(s, end="")
[docs]
def distance(a: QPointF, b: QPointF) -> float:
"""Calculates distance between a and b
:param a: Point A
:param b: Point B
:return: Distance
:rtype: float
"""
out = b - a
return math.sqrt(abs(out.x()**2 + out.y()**2))
[docs]
def draw_line(pointa: QPoint, pointb: QPoint, callback: Callable) -> None:
"""Calls callback function for each point line intersected with
:param pointa: Line point a
:param pointb: Line point b
:param callback: Callback function(lambda QPoint:)
:return: None
"""
def plotLineLow(pointa: QPoint, pointb: QPoint, callback: Callable):
if pointa.x() > pointb.x():
pointa, pointb = pointb, pointa
d = pointb - pointa
yi = 1
if d.y() < 0:
yi = -1
d.setY(-d.y())
D = (2 * d.y()) - d.x()
y = pointa.y()
for x in range(pointa.x(), pointb.x()):
callback(QPoint(x, y))
if D > 0:
y = y + yi
D = D + (2 * (d.y() - d.x()))
else:
D = D + 2 * d.y()
def plotLineHigh(pointa: QPoint, pointb: QPoint, callback: Callable):
if pointa.y() > pointb.y():
pointa, pointb = pointb, pointa
d = pointb - pointa
xi = 1
if d.x() < 0:
xi = -1
d.setX(-d.x())
D = (2 * d.x()) - d.y()
x = pointa.x()
for y in range(pointa.y(), pointb.y()):
callback(QPoint(x, y))
if D > 0:
x = x + xi
D = D + (2 * (d.x() - d.y()))
else:
D = D + 2 * d.x()
# callback(pointa)
if abs(pointb.y() - pointa.y()) < abs(pointb.x() - pointa.x()):
plotLineLow(pointa, pointb, callback)
else:
plotLineHigh(pointa, pointb, callback)
callback(pointb)
[docs]
def insensitive_path(path) -> str | None:
"""Tries to find file in path without checking case
:param path: path to file
:return: Path to fixed file if found
:rtype: str
"""
if os.path.exists(path):
return path
dir, name = os.path.split(path)
for dir, d2, a in os.walk(dir):
for i in a:
if os.path.join(dir, i).lower() == path.lower():
return os.path.join(dir, i)
break
return None
[docs]
def fit_rect(lastpos: QPoint, pos: QPoint, shift: bool, alt: bool) -> QRect:
"""Creates rectangle from 2 points and more
Literally stolen from photoshop
:param lastpos: First rect point
:param pos: Second rect point
:param shift: Makes square
:param alt: Makes lastpos center of rectangle
:return: Result rectangle
:rtype: QRect
"""
if shift:
pos2 = pos - lastpos
absx = abs(pos2.x())
xmul = 0 if absx == 0 else (pos2.x() // absx)
absy = abs(pos2.y())
ymul = 0 if absy == 0 else (pos2.y() // absy)
if absy > absx:
pos = QPoint(lastpos.x() + absy * xmul, lastpos.y() + absy * ymul)
elif absx > absy:
pos = QPoint(lastpos.x() + absx * xmul, lastpos.y() + absx * ymul)
rect = QRect.span(lastpos, pos)
if alt:
rect = QRect.span(lastpos - (pos - lastpos), pos)
return rect
[docs]
def draw_rect(rect: QRect, hollow: bool, callback: Callable) -> None:
"""Calls callback for each point inside rectangle
:param rect: rectangle
:param hollow: call callback only on edges
:param callback: callback function(lambda QPoint:)
:return: None
"""
if hollow:
for x in range(rect.x(), rect.x() + rect.width()):
callback(QPoint(x, rect.y()))
callback(QPoint(x, rect.y() + rect.height() - 1))
for y in range(rect.y() + 1, rect.y() + rect.height() - 1):
callback(QPoint(rect.x(), y))
callback(QPoint(rect.x() + rect.width() - 1, y))
return
for x in range(rect.x(), rect.x() + rect.width()):
for y in range(rect.y(), rect.y() + rect.height()):
callback(QPoint(x, y))
[docs]
def draw_ellipse(rect: QRect, hollow: bool, callback: Callable) -> None:
"""Calls callback for each point ellipse intersects with
NOTE: Callback may be called multiple times on same point
:param rect:
:param hollow: call callback only on edges or whole ellipse
:param callback: callback function(lambda QPoint:)
:return: None
"""
width = rect.width() // 2
height = rect.height() // 2
origin = rect.center()
hh = height * height
ww = width * width
hhww = hh * ww
x0 = width
dx = 0
for x in range(-width, width + 1):
if x == -width or x == width or not hollow:
callback(QPoint(origin.x() + x, origin.y()))
for y in range(1, height + 1):
x1 = x0 - (dx - 1)
while x1 > 0:
if x1*x1*hh + y*y*ww <= hhww:
break
x1 -= 1
dx = x0 - x1
x0 = x1
if hollow:
callback(QPoint(origin.x() - x0, origin.y() - y))
callback(QPoint(origin.x() - x0, origin.y() + y))
callback(QPoint(origin.x() + x0, origin.y() - y))
callback(QPoint(origin.x() + x0, origin.y() + y))
else:
for x in range(-x0, x0 + 1):
callback(QPoint(origin.x() + x, origin.y() - y))
callback(QPoint(origin.x() + x, origin.y() + y))
if not hollow:
return
y0 = height
dy = 0
callback(QPoint(origin.x(), origin.y() - height))
callback(QPoint(origin.x(), origin.y() + height))
for x in range(1, width + 1):
y1 = y0 - (dy - 1)
while y1 > 0:
if y1*y1*ww + x*x*hh <= hhww:
break
y1 -= 1
dy = y0 - y1
y0 = y1
callback(QPoint(origin.x() - x, origin.y() - y0))
callback(QPoint(origin.x() - x, origin.y() + y0))
callback(QPoint(origin.x() + x, origin.y() - y0))
callback(QPoint(origin.x() + x, origin.y() + y0))
[docs]
def paint_svg(filename: str, color: QColor) -> str:
"""Paints svg image with caching
works with resources too
if file is already stored in cache, passes it instead
:param filename: path to file or resource
:param color: svg's color
:return: Path to coloured icon in cache
:rtype: str
"""
path, file = os.path.split(filename)
file, ex = os.path.splitext(file)
newpath = os.path.join(PATH_FILES_CACHE, f"{file}_{color.red()}_{color.green()}_{color.blue()}{ex}")
if os.path.exists(newpath):
return newpath
file = QFile(filename)
file.open(QFile.OpenModeFlag.ReadOnly)
data: QByteArray = file.readAll()
file.close()
htindex = data.indexOf(b'"', data.indexOf(b"fill=\"#")) + 1
data = data.replace(htindex, data.indexOf(b'"', htindex) - htindex, bytearray(color.name(), "utf-8"))
file = QFile(newpath)
file.open(QFile.OpenModeFlag.WriteOnly)
file.write(data)
file.close()
return newpath
[docs]
def paint_svg_qicon(filename: str, color: QColor) -> QIcon:
"""Paints svg image with caching
works with resources too
if file is already stored in cache, passes it instead
:param filename: path to file or resource
:param color: svg's color
:return: Colored icon
:rtype: QIcon
"""
return QIcon(paint_svg(filename, color))
[docs]
def paint_svg_qpixmap(filename: str, color: QColor) -> QPixmap:
"""Paints svg image with caching
works with resources too
if file is already stored in cache, passes it instead
:param filename: path to file or resource
:param color: svg's color
:return: Colored pixmap
:rtype: QPixmap
"""
return QPixmap(paint_svg(filename, color))
[docs]
def remap(x: float, in_min: float, in_max: float, out_min: float, out_max: float) -> float:
"""Converts value from one range to another
:param x: value in **in** range
:param in_min: **in** in start
:param in_max: **in** end
:param out_min: **out** start
:param out_max: **out** end
:return: value in **out** range
:rtype: float
"""
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
[docs]
def lerp(a: float, b: float, t: float) -> float: # laundmo
"""linear interpolation or something
:param a: start
:param b: end
:param t: t
:return: value interpolated between a and b
:rtype: float
"""
return (1 - t) * a + t * b
[docs]
def color_lerp(c1: QColor, c2: QColor, t: float) -> QColor:
"""Interpolates between 2 colors
:param c1: First color
:param c2: Second color
:param t: Amount to interpolate between 0 and 1
:return: Interpolated color
:rtype: float
"""
return QColor.fromRgbF(lerp(c1.redF(), c2.redF(), t),
lerp(c1.greenF(), c2.greenF(), t),
lerp(c1.blueF(), c2.blueF(), t),
lerp(c1.alphaF(), c2.alphaF(), t))
[docs]
def closest_line(pos, lastpos) -> QLineF:
"""Returns the line closest to one in 8 directions
:param pos: start of line
:param lastpos: end of line
:return: closest line to given argument
:rtype: QLineF
"""
magnitude = QLineF(lastpos, pos).length()
points = [QLineF.fromPolar(magnitude, 0).p2().toPoint(),
QLineF.fromPolar(magnitude, 45).p2().toPoint(),
QLineF.fromPolar(magnitude, 90).p2().toPoint(),
QLineF.fromPolar(magnitude, 135).p2().toPoint(),
QLineF.fromPolar(magnitude, 180).p2().toPoint(),
QLineF.fromPolar(magnitude, 225).p2().toPoint(),
QLineF.fromPolar(magnitude, 270).p2().toPoint(),
QLineF.fromPolar(magnitude, 315).p2().toPoint()]
smallest = points[0]
smallestdis = 9999
for i in points:
if QLineF(i, pos - lastpos).length() < smallestdis:
smallest = i
smallestdis = QLineF(i, pos - lastpos).length()
return QLineF(lastpos, lastpos + smallest)
[docs]
def rotate_point(point: QPointF, angle) -> QPointF:
"""Rotates point with some angle
:param point: point to rotate
:param angle: angle to rotate in degrees
:return: rotated point
:rtype: QPointF
"""
px, py = point.x(), point.y()
angle = math.radians(angle)
qx = math.cos(angle) * px - math.sin(angle) * py
qy = math.sin(angle) * px + math.cos(angle) * py
return QPointF(qx, qy)
[docs]
def circle2rect(pos: QPointF, radius: float) -> QRectF:
"""Turns position and radius into rectangle
:param pos: Position of circle
:param radius: Radius of circle
:return: Rectangle
:rtype: QRectF
"""
return QRectF(pos.x() - radius, pos.y() - radius, radius * 2, radius * 2)
[docs]
def point2polar(pos: QPointF) -> QPointF:
"""Converts from Cartesian to polar coordinates
:param pos: Position in cartesian coordinates
:return: Point in polar coordinates where x is angle and y is distance
:rtype: QPointF
"""
return QPointF(math.degrees(math.atan2(pos.y(), pos.x())), math.dist([0, 0], pos.toTuple()))
[docs]
def polar2point(pos: QPointF) -> QPointF:
"""Converts from polar to Cartesian coordinates
:param pos: Position in polar coordinates where x is angle and y is distance
:return: Point in Cartesian coordinates
:rtype: QPointF
"""
nq = math.radians((pos.x() + 90) % 360)
return QPointF(math.sin(nq) * pos.y(), -math.cos(nq) * pos.y())
[docs]
def modify_path_url(path: str) -> str:
"""converts specified path to file url
:param path: path to convert
:return: converted path
:rtype: str
"""
return "file:///" + path.replace("\\", "/").replace("#", "%23")
# Source - https://stackoverflow.com/questions/4309607/whats-the-preferred-way-to-implement-a-hook-or-callback-in-python
# Posted by kindall
# Retrieved 11/5/2025, License - CC-BY-SA 4.0
[docs]
class Delegate(object):
"""
:deprecated:
Delegate allows for adding hooks to methods
!!!Does not work for methods!!!
To make method hookable, add @Delegate attribute
To hook prefix to delegate, use *= or @method.prefix attribute
To hook postfix to delegate, use += or @method.postfix attribute
Prefixes get called before delegate and can change arguments before passing it to delegate
- simply return (args, kwargs, returnval)
- if returnval is not None, stops delegate from being called
Postfixes can change return value of delegate by returning not None and can get value of delegate or previous postfix
To unhook your method from delegate, use -=
"""
[docs]
def __init__(self, func):
self.prefixes = []
self.postfixes = []
self.basefunc = func
if callable(func):
print(func.__qualname__)
def __iadd__(self, func):
if callable(func):
self.__isub__(func)
self.postfixes.append(func)
return self
def __imul__(self, func):
if callable(func):
self.__idiv__(func)
self.prefixes.append(func)
return self
[docs]
def prefix(self, func):
if callable(func):
self.__isub__(func)
self.postfixes.append(func)
return func
[docs]
def postfix(self, func):
if callable(func):
self.__isub__(func)
self.postfixes.append(func)
return func
def __isub__(self, func):
try:
if func in self.prefixes:
self.prefixes.remove(func)
return self
self.postfixes.remove(func)
except ValueError:
pass
return self
def __call__(self, *args, **kwargs):
for func in self.prefixes:
ret = func(*args, **kwargs)
if ret is None:
continue
if len(ret) != 2:
continue
args = ret[0]
kwargs = ret[1]
retval = ret[2]
if retval is not None:
return retval
print(self.__class__, self.basefunc.__class__)
result = self.basefunc(*args, **kwargs)
for func in self.postfixes:
newresult = func(*args, **kwargs, _result=result)
result = result if newresult is None else newresult
return result
[docs]
def inject_method(func, newfunc, self):
"""
:deprecated:
:param func:
:param newfunc:
:param self:
:return:
"""
def a(*args, **kwargs):
newfunc(self, func, *args, **kwargs)
return a