From: mar77i Date: Wed, 5 Feb 2025 00:11:36 +0000 (+0100) Subject: split up vectors module too X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=0b81d1bcbcd1ce8689264ce4e3bcc336c543ea4b;p=zenbook_gui split up vectors module too --- diff --git a/vectors/__init__.py b/vectors/__init__.py index e69de29..3d850b7 100644 --- a/vectors/__init__.py +++ b/vectors/__init__.py @@ -0,0 +1,10 @@ +from .base import Shape, Shapes +from .circle import Circle +from .ellipse import Ellipse +from .polygon import Polygon +from .rect import Rect +from .stroke_circle import StrokeCircle +from .stroke_circle_segment import StrokeCircleSegment +from .stroke_path import StrokePath +from .stroke_round_line import StrokeRoundLine +from .stroke_square_line import StrokeSquareLine diff --git a/vectors/base.py b/vectors/base.py new file mode 100644 index 0000000..5d4ccf8 --- /dev/null +++ b/vectors/base.py @@ -0,0 +1,20 @@ +class Shape: + def fit(self, pos, unit): + raise NotImplementedError + + def draw(self, surf, color): + pass + + +class Shapes(Shape): + def __init__(self, shapes): + self.shapes = list(shapes) + + def fit(self, pos, unit): + if type(self) != Shapes: + raise NotImplementedError + return Shapes([s.fit(pos, unit) for s in self.shapes]) + + def draw(self, surf, color): + for shape in self.shapes: + shape.draw(surf, color) diff --git a/vectors/circle.py b/vectors/circle.py new file mode 100644 index 0000000..e04b4a9 --- /dev/null +++ b/vectors/circle.py @@ -0,0 +1,30 @@ +from .base import Shape +from .ellipse import Ellipse + + +class Circle(Shape): + def __init__(self, center, radius): + self.center = center + self.radius = radius + + def fit(self, pos, unit): + if type(self) != Circle: + raise NotImplementedError + if unit[0] == unit[1]: + return Circle( + ( + pos[0] + self.center[0] * unit[0], + pos[1] + self.center[1] * unit[1], + ), + self.radius * unit[0], + ) + return Ellipse( + ( + round(pos[0] + (self.center[0] - self.radius) * unit[0]), + round(pos[1] + (self.center[1] - self.radius) * unit[1]), + ), + (round(self.radius * 2 * unit[0]), round(self.radius * 2 * unit[1])), + ) + + def draw(self, surf, color): + pygame.draw.circle(surf, color, self.center, self.radius) diff --git a/vectors/ellipse.py b/vectors/ellipse.py new file mode 100644 index 0000000..74ca1d4 --- /dev/null +++ b/vectors/ellipse.py @@ -0,0 +1,10 @@ +import pygame + +from .rect import Rect + + +class Ellipse(Rect): + def draw(self, surf, color): + pygame.draw.ellipse( + surf, color, pygame.Rect(self.left, self.top, self.width, self.height) + ) diff --git a/vectors/polygon.py b/vectors/polygon.py new file mode 100644 index 0000000..a4b747b --- /dev/null +++ b/vectors/polygon.py @@ -0,0 +1,21 @@ +import pygame + +from .base import Shape + + +class Polygon(Shape): + def __init__(self, points): + self.points = list(points) + + def fit(self, pos, unit): + if type(self) != Polygon: + raise NotImplementedError + return Polygon( + [ + (round(pos[0] + point[0] * unit[0]), round(pos[1] + point[1] * unit[1])) + for point in self.points + ] + ) + + def draw(self, surf, color): + pygame.draw.polygon(surf, color, self.points) diff --git a/vectors/rect.py b/vectors/rect.py new file mode 100644 index 0000000..e9011ec --- /dev/null +++ b/vectors/rect.py @@ -0,0 +1,24 @@ +from itertools import chain + +import pygame + +from .base import Shape + + +class Rect(Shape): + def __init__(self, *args): + if len(args) == 2: + args = tuple(chain.from_iterable(args)) + self.left, self.top, self.width, self.height = args + + def fit(self, pos, unit): + klass = type(self) + return klass( + (round(pos[0] + self.left * unit[0]), round(pos[1] + self.top * unit[1])), + (round(self.width * unit[0]), round(self.height * unit[1])), + ) + + def draw(self, surf, color): + pygame.draw.rect( + surf, color, pygame.Rect(self.left, self.top, self.width, self.height) + ) diff --git a/vectors/stroke_circle.py b/vectors/stroke_circle.py new file mode 100644 index 0000000..fdb12a9 --- /dev/null +++ b/vectors/stroke_circle.py @@ -0,0 +1,31 @@ +from math import cos, sin, tau + +from .stroke_path import StrokePath +from .stroke_round_line import StrokeRoundLine + + +class StrokeCircle(StrokePath): + def __init__(self, center, radius, width): + self.center = center + self.radius = radius + super().__init__([], True, width) + self.points.extend(self.get_points()) + + def get_points(self): + num_vertices = StrokeRoundLine.corners_needed_to_appear_round(self.radius) + return [ + ( + self.center[0] + cos(a) * self.radius, + self.center[1] + sin(a) * self.radius, + ) + for a in (tau * i / num_vertices for i in range(num_vertices)) + ] + + def fit(self, pos, unit): + if type(self) != StrokeCircle: + raise NotImplementedError + return StrokeCircle( + (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), + self.radius * max(unit), + self.width * max(unit), + ) diff --git a/vectors/stroke_circle_segment.py b/vectors/stroke_circle_segment.py new file mode 100644 index 0000000..d3eddd1 --- /dev/null +++ b/vectors/stroke_circle_segment.py @@ -0,0 +1,44 @@ +from math import cos, sin, tau + + +from .stroke_circle import StrokeCircle +from .stroke_round_line import StrokeRoundLine + + +class StrokeCircleSegment(StrokeCircle): + def __init__(self, center, radius, start_angle, end_angle, width): + self.start_angle = start_angle + self.end_angle = end_angle + super().__init__(center, radius, width) + self.closed = False + + def get_points(self): + return [ + (self.center[0] + cos(a) * self.radius, self.center[1] + sin(a) * self.radius) + for a in self.get_angle_segments( + self.start_angle, + self.end_angle, + StrokeRoundLine.corners_needed_to_appear_round((self.radius + self.width / 2)), + ) + ] + + def get_angle_segments(self, start_angle, end_angle, num_vertices): + start_angle, end_angle = sorted((start_angle, end_angle)) + diff_angle = end_angle - start_angle + for i in range(num_vertices): + a = tau * i / num_vertices + if a >= diff_angle: + yield end_angle + break + yield start_angle + a + + def fit(self, pos, unit): + if type(self) != StrokeCircleSegment: + raise NotImplementedError + return StrokeCircleSegment( + (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), + self.radius * max(unit), + self.start_angle, + self.end_angle, + self.width * max(unit), + ) diff --git a/vectors/stroke_path.py b/vectors/stroke_path.py new file mode 100644 index 0000000..6ea9e80 --- /dev/null +++ b/vectors/stroke_path.py @@ -0,0 +1,39 @@ +from .base import Shapes +from .stroke_round_line import StrokeRoundLine + + +class StrokePath: + def __init__(self, points, closed, width): + self.points = list(points) + self.closed = bool(closed) + self.width = width + + @property + def shapes(self): + if not self.points: + return + if self.closed: + yield StrokeRoundLine( + self.points[-1], + self.points[0], + self.width, + ) + iter_points = iter(self.points) + old = next(iter_points) + for new in iter_points: + yield StrokeRoundLine(old, new, self.width) + old = new + + def fit(self, pos, unit): + if type(self) != StrokePath: + raise NotImplementedError + return StrokePath( + [ + (pos[0] + point[0] * unit[0], pos[1] + point[1] * unit[1]) + for point in self.points + ], + self.closed, + self.width, + ) + + draw = Shapes.draw diff --git a/vectors/stroke_round_line.py b/vectors/stroke_round_line.py new file mode 100644 index 0000000..df3e40a --- /dev/null +++ b/vectors/stroke_round_line.py @@ -0,0 +1,65 @@ +from math import acos, atan2, ceil, cos, floor, pi, sin, sqrt, tau + +from .polygon import Polygon + + +class StrokeRoundLine(Polygon): + """ + Enclose a rectangle fitted between two points with a regular, non-rotated n-gon + that should be visually indistinguishable with a half circle + """ + def __init__(self, p1, p2, width): + self.p1 = p1 + self.p2 = p2 + self.width = width + super().__init__(self.get_points()) + + def get_points(self): + radius = self.width / 2 + num_vertices = self.corners_needed_to_appear_round(radius) + delta = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) + distance = sqrt(delta[0] * delta[0] + delta[1] * delta[1]) + points = [] + if distance == 0: + initial_corner = opposing_corner = num_vertices + back = (0, 0) + else: + initial_corner = ceil( + atan2(-delta[0], delta[1]) * num_vertices / tau + ) + opposing_corner = ( + floor(atan2(delta[0], -delta[1]) * num_vertices / tau) - initial_corner + ) % num_vertices + back = (delta[0] * radius / distance, delta[1] * radius / distance) + points.extend( + ( + (self.p2[0] + back[1], self.p2[1] - back[0]), + (self.p1[0] + back[1], self.p1[1] - back[0]), + ) + ) + p = self.p1 + for i in range(num_vertices): + angle = (i + initial_corner) * tau / num_vertices + points.append((p[0] + cos(angle) * radius, p[1] + sin(angle) * radius)) + if i == opposing_corner: + points.extend( + ( + (self.p1[0] - back[1], self.p1[1] + back[0]), + (self.p2[0] - back[1], self.p2[1] + back[0]), + ) + ) + p = self.p2 + return points + + @staticmethod + def corners_needed_to_appear_round(radius, threshold=.5): + return ceil(pi / acos(1 - threshold / radius)) + + def fit(self, pos, unit): + if type(self) != StrokeRoundLine: + raise NotImplementedError + return StrokeRoundLine( + (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), + (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), + self.width * max(unit), + ) diff --git a/vectors/stroke_square_line.py b/vectors/stroke_square_line.py new file mode 100644 index 0000000..975f0b8 --- /dev/null +++ b/vectors/stroke_square_line.py @@ -0,0 +1,43 @@ +from math import sqrt + +from .polygon import Polygon + + +class StrokeSquareLine(Polygon): + """ + Rotate a rectangle along the direction in which we're drawing. + """ + def __init__(self, p1, p2, width): + self.p1 = p1 + self.p2 = p2 + self.width = width + super().__init__(self.get_points()) + + def get_points(self): + p_rel = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) + twice_distance = 2 * sqrt(p_rel[0] * p_rel[0] + p_rel[1] * p_rel[1]) + if twice_distance: + back = ( + p_rel[0] * self.width / twice_distance, + p_rel[1] * self.width / twice_distance, + ) + back_sum, back_diff = back[0] + back[1], back[0] - back[1] + else: + back_sum, back_diff = -self.width / 2, self.width / 2 + # left is aka: (back[1], -back[0]) + # right is aka: (-back[1], back[0]) + return [ + (self.p1[0] + back_sum, self.p1[1] - back_diff), + (self.p1[0] + back_diff, self.p1[1] + back_sum), + (self.p2[0] - back_sum, self.p2[1] + back_diff), + (self.p2[0] - back_diff, self.p2[1] - back_sum), + ] + + def fit(self, pos, unit): + if type(self) != StrokeSquareLine: + raise NotImplementedError + return StrokeSquareLine( + (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), + (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), + self.width * max(unit), + ) diff --git a/vectors/vectors.py b/vectors/vectors.py deleted file mode 100644 index b3bf98f..0000000 --- a/vectors/vectors.py +++ /dev/null @@ -1,305 +0,0 @@ -from itertools import chain -from math import acos, atan2, ceil, cos, floor, pi, sin, sqrt, tau - -import pygame - - -class Shape: - def fit(self, pos, unit): - raise NotImplementedError - - def draw(self, surf, color): - pass - - -class Rect(Shape): - def __init__(self, *args): - if len(args) == 2: - args = tuple(chain.from_iterable(args)) - self.left, self.top, self.width, self.height = args - - def fit(self, pos, unit): - klass = type(self) - if klass not in (Rect, Ellipse): - raise NotImplementedError - return klass( - (round(pos[0] + self.left * unit[0]), round(pos[1] + self.top * unit[1])), - (round(self.width * unit[0]), round(self.height * unit[1])), - ) - - def draw(self, surf, color): - pygame.draw.rect( - surf, color, pygame.Rect(self.left, self.top, self.width, self.height) - ) - - -class Ellipse(Rect): - def draw(self, surf, color): - pygame.draw.ellipse( - surf, color, pygame.Rect(self.left, self.top, self.width, self.height) - ) - - -class Polygon(Shape): - def __init__(self, points): - self.points = list(points) - - def fit(self, pos, unit): - if type(self) != Polygon: - raise NotImplementedError - return Polygon( - [ - (round(pos[0] + point[0] * unit[0]), round(pos[1] + point[1] * unit[1])) - for point in self.points - ] - ) - - def draw(self, surf, color): - pygame.draw.polygon(surf, color, self.points) - - -class Circle(Shape): - def __init__(self, center, radius): - self.center = center - self.radius = radius - - def fit(self, pos, unit): - if type(self) != Circle: - raise NotImplementedError - if unit[0] == unit[1]: - return Circle( - ( - pos[0] + self.center[0] * unit[0], - pos[1] + self.center[1] * unit[1], - ), - self.radius * unit[0], - ) - return Ellipse( - ( - round(pos[0] + (self.center[0] - self.radius) * unit[0]), - round(pos[1] + (self.center[1] - self.radius) * unit[1]), - ), - (round(self.radius * 2 * unit[0]), round(self.radius * 2 * unit[1])), - ) - - def draw(self, surf, color): - pygame.draw.circle(surf, color, self.center, self.radius) - - -class StrokeSquareLine(Polygon): - """ - Rotate a rectangle along the direction in which we're drawing. - """ - def __init__(self, p1, p2, width): - self.p1 = p1 - self.p2 = p2 - self.width = width - super().__init__(self.get_points()) - - def get_points(self): - p_rel = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) - twice_distance = 2 * sqrt(p_rel[0] * p_rel[0] + p_rel[1] * p_rel[1]) - if twice_distance: - back = ( - p_rel[0] * self.width / twice_distance, - p_rel[1] * self.width / twice_distance, - ) - back_sum, back_diff = back[0] + back[1], back[0] - back[1] - else: - back_sum, back_diff = -self.width / 2, self.width / 2 - # left is aka: (back[1], -back[0]) - # right is aka: (-back[1], back[0]) - return [ - (self.p1[0] + back_sum, self.p1[1] - back_diff), - (self.p1[0] + back_diff, self.p1[1] + back_sum), - (self.p2[0] - back_sum, self.p2[1] + back_diff), - (self.p2[0] - back_diff, self.p2[1] - back_sum), - ] - - def fit(self, pos, unit): - if type(self) != StrokeSquareLine: - raise NotImplementedError - return StrokeSquareLine( - (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), - (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), - self.width * max(unit), - ) - - -class StrokeRoundLine(Polygon): - """ - Enclose a rectangle fitted between two points with a regular, non-rotated n-gon - that should be visually indistinguishable with a half circle - """ - def __init__(self, p1, p2, width): - self.p1 = p1 - self.p2 = p2 - self.width = width - super().__init__(self.get_points()) - - def get_points(self): - radius = self.width / 2 - num_vertices = self.corners_needed_to_appear_round(radius) - delta = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) - distance = sqrt(delta[0] * delta[0] + delta[1] * delta[1]) - points = [] - if distance == 0: - initial_corner = opposing_corner = num_vertices - back = (0, 0) - else: - initial_corner = ceil( - atan2(-delta[0], delta[1]) * num_vertices / tau - ) - opposing_corner = ( - floor(atan2(delta[0], -delta[1]) * num_vertices / tau) - initial_corner - ) % num_vertices - back = (delta[0] * radius / distance, delta[1] * radius / distance) - points.extend( - ( - (self.p2[0] + back[1], self.p2[1] - back[0]), - (self.p1[0] + back[1], self.p1[1] - back[0]), - ) - ) - p = self.p1 - for i in range(num_vertices): - angle = (i + initial_corner) * tau / num_vertices - points.append((p[0] + cos(angle) * radius, p[1] + sin(angle) * radius)) - if i == opposing_corner: - points.extend( - ( - (self.p1[0] - back[1], self.p1[1] + back[0]), - (self.p2[0] - back[1], self.p2[1] + back[0]), - ) - ) - p = self.p2 - return points - - @staticmethod - def corners_needed_to_appear_round(radius, threshold=.5): - return ceil(pi / acos(1 - threshold / radius)) - - def fit(self, pos, unit): - if type(self) != StrokeRoundLine: - raise NotImplementedError - return StrokeRoundLine( - (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), - (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), - self.width * max(unit), - ) - - -class Shapes(Shape): - def __init__(self, shapes): - self.shapes = list(shapes) - - def fit(self, pos, unit): - if type(self) != Shapes: - raise NotImplementedError - return Shapes([s.fit(pos, unit) for s in self.shapes]) - - def draw(self, surf, color): - for shape in self.shapes: - shape.draw(surf, color) - - -class StrokePath: - def __init__(self, points, closed, width): - self.points = list(points) - self.closed = bool(closed) - self.width = width - - @property - def shapes(self): - if not self.points: - return - if self.closed: - yield StrokeRoundLine( - self.points[-1], - self.points[0], - self.width, - ) - iter_points = iter(self.points) - old = next(iter_points) - for new in iter_points: - yield StrokeRoundLine(old, new, self.width) - old = new - - def fit(self, pos, unit): - if type(self) != StrokePath: - raise NotImplementedError - return StrokePath( - [ - (pos[0] + point[0] * unit[0], pos[1] + point[1] * unit[1]) - for point in self.points - ], - self.closed, - self.width, - ) - - draw = Shapes.draw - - -class StrokeCircle(StrokePath): - def __init__(self, center, radius, width): - self.center = center - self.radius = radius - super().__init__([], True, width) - self.points.extend(self.get_points()) - - def get_points(self): - num_vertices = StrokeRoundLine.corners_needed_to_appear_round(self.radius) - return [ - ( - self.center[0] + cos(a) * self.radius, - self.center[1] + sin(a) * self.radius, - ) - for a in (tau * i / num_vertices for i in range(num_vertices)) - ] - - def fit(self, pos, unit): - if type(self) != StrokeCircle: - raise NotImplementedError - return StrokeCircle( - (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), - self.radius * max(unit), - self.width * max(unit), - ) - - -class StrokeCircleSegment(StrokeCircle): - def __init__(self, center, radius, start_angle, end_angle, width): - self.start_angle = start_angle - self.end_angle = end_angle - super().__init__(center, radius, width) - self.closed = False - - def get_points(self): - return [ - (self.center[0] + cos(a) * self.radius, self.center[1] + sin(a) * self.radius) - for a in self.get_angle_segments( - self.start_angle, - self.end_angle, - StrokeRoundLine.corners_needed_to_appear_round((self.radius + self.width / 2)), - ) - ] - - def get_angle_segments(self, start_angle, end_angle, num_vertices): - start_angle, end_angle = sorted((start_angle, end_angle)) - diff_angle = end_angle - start_angle - for i in range(num_vertices): - a = tau * i / num_vertices - if a >= diff_angle: - yield end_angle - break - yield start_angle + a - - def fit(self, pos, unit): - if type(self) != StrokeCircleSegment: - raise NotImplementedError - return StrokeCircleSegment( - (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), - self.radius * max(unit), - self.start_angle, - self.end_angle, - self.width * max(unit), - ) diff --git a/zenbook_conf/shapes.py b/zenbook_conf/shapes.py index a9c7c6d..4d7be4b 100644 --- a/zenbook_conf/shapes.py +++ b/zenbook_conf/shapes.py @@ -1,6 +1,6 @@ from math import cos, pi, sin, tau -from vectors.vectors import ( +from vectors import ( Polygon, Rect, Shapes, StrokeCircleSegment, StrokeRoundLine, StrokeSquareLine ) diff --git a/zenbook_conf/zenbook_conf.py b/zenbook_conf/zenbook_conf.py index ff67292..b950063 100644 --- a/zenbook_conf/zenbook_conf.py +++ b/zenbook_conf/zenbook_conf.py @@ -3,7 +3,7 @@ from functools import partial import pygame from .bluetooth import BluetoothConf -from ui.ui import Button, FPSWidget, Icon, IconButton, Root, Switch +from ui import Button, FPSWidget, Icon, IconButton, Root, Switch from .shapes import ( bluetooth, laptop_double,