From 77ddc5c7fd2ae5dd9bc4f1400e8c374d2e113cea Mon Sep 17 00:00:00 2001 From: Quinn Date: Tue, 27 Aug 2024 18:01:26 -0400 Subject: [PATCH 1/9] starting to build the naimation tool --- tools/animation-tools/animation-tool.py | 207 ++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 tools/animation-tools/animation-tool.py diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py new file mode 100644 index 0000000..4158a54 --- /dev/null +++ b/tools/animation-tools/animation-tool.py @@ -0,0 +1,207 @@ +import math +import pygame +from itertools import product +from pygame_widgets.slider import Slider +import pygame_widgets + +def project(vector, w, h, fov, distance): + factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z) + x = vector.x * factor * w + w / 2 + y = -vector.y * factor * w + h / 2 + return pygame.math.Vector3(x, y, vector.z) + +def rotate_vertices(vertices, angle, axis): + return [v.rotate(angle, axis) for v in vertices] +def scale_vertices(vertices, s): + return [pygame.math.Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices] +def translate_vertices(vertices, t): + return [v + pygame.math.Vector3(t) for v in vertices] +def project_vertices(vertices, w, h, fov, distance): + return [project(v, w, h, fov, distance) for v in vertices] + +class Mesh(): + def __init__(self, vertices, faces): + self.__vertices = [pygame.math.Vector3(v) for v in vertices] + self.__faces = faces + self.face_color = (0, 0, 0) + self.edge_color = (0,0,0) + + def rotate(self, angle, axis): + self.__vertices = rotate_vertices(self.__vertices, angle, axis) + def scale(self, s): + self.__vertices = scale_vertices(self.__vertices, s) + def translate(self, t): + self.__vertices = translate_vertices(self.__vertices, t) + + def calculate_average_z(self, vertices): + return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)] + + def get_face(self, index): + return self.__faces[index] + def get_vertices(self): + return self.__vertices + + def create_polygon(self, face, vertices): + return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]] + + def set_face_color(self, color : tuple[int]): + self.face_color = color + + def set_edge_color(self, color : tuple[int]): + self.edge_color = color + +class Scene: + def __init__(self, meshes, fov, distance): + self.meshes: list[Mesh] = meshes + self.fov = fov + self.distance = distance + self.euler_angles = [0, 0, 0] + + def transform_vertices(self, vertices, width, height): + transformed_vertices = vertices + axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))): + transformed_vertices = rotate_vertices(transformed_vertices, angle, axis) + transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance) + return transformed_vertices + + def point_in_polygon(self, point, polygon): + """ Determine if the point (x, y) is inside the polygon """ + x, y = point + n = len(polygon) + inside = False + p1x, p1y = polygon[0] + for i in range(n + 1): + p2x, p2y = polygon[i % n] + if y > min(p1y, p2y): + if y <= max(p1y, p2y): + if x <= max(p1x, p2x): + if p1y != p2y: + xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x + if p1x == p2x or x <= xints: + inside = not inside + p1x, p1y = p2x, p2y + return inside + + def get_mesh_from_xy(self, pos: tuple[int]) -> Mesh: + x, y = pos + closest_mesh = None + closest_z = float('inf') + + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *pygame.display.get_surface().get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for i, avg_z_value in avg_z: + polygon = mesh.create_polygon(mesh.get_face(i), transformed_vertices) + if self.point_in_polygon((x, y), polygon): + if avg_z_value < closest_z: + closest_z = avg_z_value + closest_mesh = mesh + + return closest_mesh + + def draw(self, surface): + + polygons = [] + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for z in avg_z: + #for z in sorted(avg_z, key=lambda x: x[1], reverse=True): + pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices) + polygons.append((pointlist, z[1], mesh.face_color, mesh.edge_color)) + #pygame.draw.polygon(surface, (128, 128, 192), pointlist) + #pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3) + + for poly in sorted(polygons, key=lambda x: x[1], reverse=True): + pygame.draw.polygon(surface, poly[2], poly[0]) + pygame.draw.polygon(surface, poly[3], poly[0], 3) + + + +class Cube(Mesh): + def __init__(self): + vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)] + faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)] + super().__init__(vertices, faces) + + def set_position(self, position : tuple): + super().translate(position) + +class ColorPicker: + def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): + width = max(50, width) + self.sliders: list[Slider] = [ + Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True) for i in range(3) + ] + for slider in self.sliders: + slider.enable() + + def get_color(self) -> tuple[int]: + # return (0,0,0) + return tuple([slider.getValue() for slider in self.sliders]) + + def set_color(self, color: tuple[int]): + for i, slider in enumerate(self.sliders): + slider.setValue(color[i]) + +meshes: list[Cube] = [] +for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): + cube = Cube() + cube.scale((0.35, 0.35, 0.35)) + cube.set_position(origin) + cube.set_face_color((0, 0, 0)) + meshes.append(cube) + +scene = Scene(meshes, 90, 5) + + +pygame.init() +window = pygame.display.set_mode((500, 500)) +colorPicker = ColorPicker(window, 20, 20, 100, 300) +clock = pygame.time.Clock() +selected_meshes = [] +run = True +trackMouseMotion = False + +while run: + clock.tick(60) + window.fill((255, 255, 255)) + events = pygame.event.get() + for event in events: + if event.type == pygame.QUIT: + run = False + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 2: # middle mouse click + trackMouseMotion = True + elif event.button == 1: # left click + mousePosition = pygame.mouse.get_pos() + mesh = scene.get_mesh_from_xy(mousePosition) + if mesh != None: + if mesh in selected_meshes: + mesh.set_edge_color((0,0,0)) + selected_meshes.remove(mesh) + else: + mesh.set_edge_color((255,0,0)) + selected_meshes.append(mesh) + + pass + elif event.type == pygame.MOUSEBUTTONUP: + if event.button == 2: # middle mouse release + trackMouseMotion = False + elif trackMouseMotion and event.type == pygame.MOUSEMOTION: + mouseMovement = pygame.mouse.get_rel() + scene.euler_angles[0] -= mouseMovement[1] + scene.euler_angles[1] -= mouseMovement[0] + pygame_widgets.update(events) + + + if not trackMouseMotion: + pygame.mouse.get_rel() + + for mesh in selected_meshes: + mesh.set_face_color(colorPicker.get_color()) + scene.draw(window) + pygame.display.flip() + +pygame.quit() \ No newline at end of file From 7a7494beb2afb6e5274c433db2bf563d4af58a5f Mon Sep 17 00:00:00 2001 From: Quinn Date: Tue, 27 Aug 2024 18:05:09 -0400 Subject: [PATCH 2/9] Set up virtual environment --- tools/animation-tools/.gitignore | 1 + tools/animation-tools/requirements.txt | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 tools/animation-tools/.gitignore create mode 100644 tools/animation-tools/requirements.txt diff --git a/tools/animation-tools/.gitignore b/tools/animation-tools/.gitignore new file mode 100644 index 0000000..eba74f4 --- /dev/null +++ b/tools/animation-tools/.gitignore @@ -0,0 +1 @@ +venv/ \ No newline at end of file diff --git a/tools/animation-tools/requirements.txt b/tools/animation-tools/requirements.txt new file mode 100644 index 0000000..4455b2b --- /dev/null +++ b/tools/animation-tools/requirements.txt @@ -0,0 +1,2 @@ +pygame==2.6.0 +pygame-widgets==1.1.5 From 7a454edc469849048e890a5f2faa8d950f48fd32 Mon Sep 17 00:00:00 2001 From: Quinn Date: Tue, 27 Aug 2024 18:17:27 -0400 Subject: [PATCH 3/9] Organized modules into individual files --- tools/animation-tools/.gitignore | 3 +- tools/animation-tools/CustomWidgets.py | 18 +++ tools/animation-tools/MatrixMath.py | 17 +++ tools/animation-tools/Scene.py | 70 +++++++++++ tools/animation-tools/Shapes.py | 42 +++++++ tools/animation-tools/animation-tool.py | 148 +----------------------- 6 files changed, 154 insertions(+), 144 deletions(-) create mode 100644 tools/animation-tools/CustomWidgets.py create mode 100644 tools/animation-tools/MatrixMath.py create mode 100644 tools/animation-tools/Scene.py create mode 100644 tools/animation-tools/Shapes.py diff --git a/tools/animation-tools/.gitignore b/tools/animation-tools/.gitignore index eba74f4..4ea05a1 100644 --- a/tools/animation-tools/.gitignore +++ b/tools/animation-tools/.gitignore @@ -1 +1,2 @@ -venv/ \ No newline at end of file +venv/ +__pycache__/ \ No newline at end of file diff --git a/tools/animation-tools/CustomWidgets.py b/tools/animation-tools/CustomWidgets.py new file mode 100644 index 0000000..395baa8 --- /dev/null +++ b/tools/animation-tools/CustomWidgets.py @@ -0,0 +1,18 @@ +from pygame_widgets.slider import Slider + +class ColorPicker: + def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): + width = max(50, width) + self.sliders: list[Slider] = [ + Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True) for i in range(3) + ] + for slider in self.sliders: + slider.enable() + + def get_color(self) -> tuple[int]: + # return (0,0,0) + return tuple([slider.getValue() for slider in self.sliders]) + + def set_color(self, color: tuple[int]): + for i, slider in enumerate(self.sliders): + slider.setValue(color[i]) \ No newline at end of file diff --git a/tools/animation-tools/MatrixMath.py b/tools/animation-tools/MatrixMath.py new file mode 100644 index 0000000..cd0d371 --- /dev/null +++ b/tools/animation-tools/MatrixMath.py @@ -0,0 +1,17 @@ +import math +from pygame.math import Vector3 + +def project(vector, w, h, fov, distance): + factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z) + x = vector.x * factor * w + w / 2 + y = -vector.y * factor * w + h / 2 + return Vector3(x, y, vector.z) + +def rotate_vertices(vertices, angle, axis): + return [v.rotate(angle, axis) for v in vertices] +def scale_vertices(vertices, s): + return [Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices] +def translate_vertices(vertices, t): + return [v + Vector3(t) for v in vertices] +def project_vertices(vertices, w, h, fov, distance): + return [project(v, w, h, fov, distance) for v in vertices] \ No newline at end of file diff --git a/tools/animation-tools/Scene.py b/tools/animation-tools/Scene.py new file mode 100644 index 0000000..a5fe9db --- /dev/null +++ b/tools/animation-tools/Scene.py @@ -0,0 +1,70 @@ +from Shapes import Mesh +from MatrixMath import * +import pygame + +class Scene: + def __init__(self, meshes, fov, distance): + self.meshes: list[Mesh] = meshes + self.fov = fov + self.distance = distance + self.euler_angles = [0, 0, 0] + + def transform_vertices(self, vertices, width, height): + transformed_vertices = vertices + axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))): + transformed_vertices = rotate_vertices(transformed_vertices, angle, axis) + transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance) + return transformed_vertices + + def point_in_polygon(self, point, polygon): + """ Determine if the point (x, y) is inside the polygon """ + x, y = point + n = len(polygon) + inside = False + p1x, p1y = polygon[0] + for i in range(n + 1): + p2x, p2y = polygon[i % n] + if y > min(p1y, p2y): + if y <= max(p1y, p2y): + if x <= max(p1x, p2x): + if p1y != p2y: + xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x + if p1x == p2x or x <= xints: + inside = not inside + p1x, p1y = p2x, p2y + return inside + + def get_mesh_from_xy(self, pos: tuple[int]) -> Mesh: + x, y = pos + closest_mesh = None + closest_z = float('inf') + + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *pygame.display.get_surface().get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for i, avg_z_value in avg_z: + polygon = mesh.create_polygon(mesh.get_face(i), transformed_vertices) + if self.point_in_polygon((x, y), polygon): + if avg_z_value < closest_z: + closest_z = avg_z_value + closest_mesh = mesh + + return closest_mesh + + def draw(self, surface): + + polygons = [] + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for z in avg_z: + #for z in sorted(avg_z, key=lambda x: x[1], reverse=True): + pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices) + polygons.append((pointlist, z[1], mesh.face_color, mesh.edge_color)) + #pygame.draw.polygon(surface, (128, 128, 192), pointlist) + #pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3) + + for poly in sorted(polygons, key=lambda x: x[1], reverse=True): + pygame.draw.polygon(surface, poly[2], poly[0]) + pygame.draw.polygon(surface, poly[3], poly[0], 3) \ No newline at end of file diff --git a/tools/animation-tools/Shapes.py b/tools/animation-tools/Shapes.py new file mode 100644 index 0000000..5280177 --- /dev/null +++ b/tools/animation-tools/Shapes.py @@ -0,0 +1,42 @@ +from MatrixMath import * +from pygame.math import Vector3 + +class Mesh(): + def __init__(self, vertices, faces): + self.__vertices = [Vector3(v) for v in vertices] + self.__faces = faces + self.face_color = (0, 0, 0) + self.edge_color = (0,0,0) + + def rotate(self, angle, axis): + self.__vertices = rotate_vertices(self.__vertices, angle, axis) + def scale(self, s): + self.__vertices = scale_vertices(self.__vertices, s) + def translate(self, t): + self.__vertices = translate_vertices(self.__vertices, t) + + def calculate_average_z(self, vertices): + return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)] + + def get_face(self, index): + return self.__faces[index] + def get_vertices(self): + return self.__vertices + + def create_polygon(self, face, vertices): + return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]] + + def set_face_color(self, color : tuple[int]): + self.face_color = color + + def set_edge_color(self, color : tuple[int]): + self.edge_color = color + +class Cube(Mesh): + def __init__(self): + vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)] + faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)] + super().__init__(vertices, faces) + + def set_position(self, position : tuple): + super().translate(position) \ No newline at end of file diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py index 4158a54..4aad042 100644 --- a/tools/animation-tools/animation-tool.py +++ b/tools/animation-tools/animation-tool.py @@ -1,151 +1,13 @@ -import math import pygame from itertools import product -from pygame_widgets.slider import Slider import pygame_widgets - -def project(vector, w, h, fov, distance): - factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z) - x = vector.x * factor * w + w / 2 - y = -vector.y * factor * w + h / 2 - return pygame.math.Vector3(x, y, vector.z) - -def rotate_vertices(vertices, angle, axis): - return [v.rotate(angle, axis) for v in vertices] -def scale_vertices(vertices, s): - return [pygame.math.Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices] -def translate_vertices(vertices, t): - return [v + pygame.math.Vector3(t) for v in vertices] -def project_vertices(vertices, w, h, fov, distance): - return [project(v, w, h, fov, distance) for v in vertices] - -class Mesh(): - def __init__(self, vertices, faces): - self.__vertices = [pygame.math.Vector3(v) for v in vertices] - self.__faces = faces - self.face_color = (0, 0, 0) - self.edge_color = (0,0,0) - - def rotate(self, angle, axis): - self.__vertices = rotate_vertices(self.__vertices, angle, axis) - def scale(self, s): - self.__vertices = scale_vertices(self.__vertices, s) - def translate(self, t): - self.__vertices = translate_vertices(self.__vertices, t) - - def calculate_average_z(self, vertices): - return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)] - - def get_face(self, index): - return self.__faces[index] - def get_vertices(self): - return self.__vertices - - def create_polygon(self, face, vertices): - return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]] - - def set_face_color(self, color : tuple[int]): - self.face_color = color - - def set_edge_color(self, color : tuple[int]): - self.edge_color = color - -class Scene: - def __init__(self, meshes, fov, distance): - self.meshes: list[Mesh] = meshes - self.fov = fov - self.distance = distance - self.euler_angles = [0, 0, 0] - - def transform_vertices(self, vertices, width, height): - transformed_vertices = vertices - axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] - for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))): - transformed_vertices = rotate_vertices(transformed_vertices, angle, axis) - transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance) - return transformed_vertices - - def point_in_polygon(self, point, polygon): - """ Determine if the point (x, y) is inside the polygon """ - x, y = point - n = len(polygon) - inside = False - p1x, p1y = polygon[0] - for i in range(n + 1): - p2x, p2y = polygon[i % n] - if y > min(p1y, p2y): - if y <= max(p1y, p2y): - if x <= max(p1x, p2x): - if p1y != p2y: - xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x - if p1x == p2x or x <= xints: - inside = not inside - p1x, p1y = p2x, p2y - return inside - - def get_mesh_from_xy(self, pos: tuple[int]) -> Mesh: - x, y = pos - closest_mesh = None - closest_z = float('inf') - - for mesh in self.meshes: - transformed_vertices = self.transform_vertices(mesh.get_vertices(), *pygame.display.get_surface().get_size()) - avg_z = mesh.calculate_average_z(transformed_vertices) - for i, avg_z_value in avg_z: - polygon = mesh.create_polygon(mesh.get_face(i), transformed_vertices) - if self.point_in_polygon((x, y), polygon): - if avg_z_value < closest_z: - closest_z = avg_z_value - closest_mesh = mesh - - return closest_mesh - - def draw(self, surface): - - polygons = [] - for mesh in self.meshes: - transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size()) - avg_z = mesh.calculate_average_z(transformed_vertices) - for z in avg_z: - #for z in sorted(avg_z, key=lambda x: x[1], reverse=True): - pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices) - polygons.append((pointlist, z[1], mesh.face_color, mesh.edge_color)) - #pygame.draw.polygon(surface, (128, 128, 192), pointlist) - #pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3) - - for poly in sorted(polygons, key=lambda x: x[1], reverse=True): - pygame.draw.polygon(surface, poly[2], poly[0]) - pygame.draw.polygon(surface, poly[3], poly[0], 3) +from Shapes import * +from Scene import Scene +from CustomWidgets import ColorPicker - -class Cube(Mesh): - def __init__(self): - vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)] - faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)] - super().__init__(vertices, faces) - - def set_position(self, position : tuple): - super().translate(position) - -class ColorPicker: - def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): - width = max(50, width) - self.sliders: list[Slider] = [ - Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True) for i in range(3) - ] - for slider in self.sliders: - slider.enable() - - def get_color(self) -> tuple[int]: - # return (0,0,0) - return tuple([slider.getValue() for slider in self.sliders]) - - def set_color(self, color: tuple[int]): - for i, slider in enumerate(self.sliders): - slider.setValue(color[i]) - -meshes: list[Cube] = [] +# generate a list of cubes +meshes: list[Mesh] = [] for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): cube = Cube() cube.scale((0.35, 0.35, 0.35)) From e4506bd9f0d824ece6f3cdd97b8d4263d5ab74fe Mon Sep 17 00:00:00 2001 From: Quinn Date: Tue, 27 Aug 2024 21:54:56 -0400 Subject: [PATCH 4/9] Got a single frame exporting correctly --- lib/Animator/Animation.h | 28 ++++++-- platformio.ini | 21 +++--- src/main.cpp | 3 +- tools/animation-tools/.gitignore | 3 +- tools/animation-tools/AnimationExporter.py | 74 ++++++++++++++++++++++ tools/animation-tools/README.md | 31 +++++++++ tools/animation-tools/Shapes.py | 6 ++ tools/animation-tools/animation-tool.py | 13 +++- 8 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 tools/animation-tools/AnimationExporter.py create mode 100644 tools/animation-tools/README.md diff --git a/lib/Animator/Animation.h b/lib/Animator/Animation.h index 4adfa02..6f78b7a 100644 --- a/lib/Animator/Animation.h +++ b/lib/Animator/Animation.h @@ -13,7 +13,7 @@ namespace AnimationHelpers{ V3D cyan{0,255,255}; V3D magenta{255,0,255}; - Cell CreateCell(float x_percent, float y_percent, float z_percent, V3D &color){ + Cell CreateCell(float x_percent, float y_percent, float z_percent, const V3D &color){ float continuousMaxValue{static_cast(std::numeric_limits::max())}; Cell cell{ .position = V3D{ @@ -211,11 +211,27 @@ namespace RisingCubes{ .delay = std::chrono::milliseconds(1) }; + AnimationFrame frame0{ + .frame = { + CreateCell(0.0,0.0,0.0,V3D(255.0,255.0,255.0)), + CreateCell(0.0,0.5,0.0,V3D(0.0,255.0,0.0)), + CreateCell(0.0,1.0,0.0,V3D(0.0,255.0,0.0)), + CreateCell(0.0,0.0,0.5,V3D(0.0,0.0,255.0)), + CreateCell(0.0,0.0,1.0,V3D(0.0,0.0,255.0)), + CreateCell(0.5,0.0,0.0,V3D(255.0,0.0,0.0)), + CreateCell(1.0,0.0,0.0,V3D(255.0,0.0,0.0)) + }, + .fillInterpolation = FillInterpolation::NO_FILL, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + std::vector rising{ - frame1, // 0 - frame2, // 1 - frame3, // 2 - frame4, // 3 - frame5 + frame0, frame0 + // frame1, // 0 + // frame2, // 1 + // frame3, // 2 + // frame4, // 3 + // frame5 }; } \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 2b70b1f..4e4922f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -7,34 +7,37 @@ ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html + [platformio] -default_envs = esp32s3_release ; this ensures that only this environment is built for anything but the debug build +default_envs = esp32s3_release extra_configs = platformio-local.ini [env] -; platform = espressif32 upload_protocol = esptool platform = https://github.com/platformio/platform-espressif32.git board = esp32-s3-devkitc-1 framework = arduino build_flags = -Iinclude - monitor_speed = 9600 monitor_filters = colorize, send_on_enter -upload_speed = 2000000 ;ESP32S3 USB-Serial Converter maximum 2000000bps -lib_deps = adafruit/Adafruit NeoPixel@^1.12.0 +upload_speed = 2000000 +lib_deps = + adafruit/Adafruit NeoPixel@^1.12.3 + fastled/FastLED@^3.7.3 [env:esp32s3_release] build_type = release +monitor_port = COM21 +upload_port = COM20 [env:esp32s3_debug] debug_init_break = tbreak setup debug_tool = esp-builtin build_type = debug debug_speed = 20000 -; debug_port = COM7 -; monitor_port = COM14 build_flags = - -D DEBUG = 1 - -I include \ No newline at end of file + -D DEBUG = 1 + -I include +monitor_port = COM21 +upload_port = COM20 diff --git a/src/main.cpp b/src/main.cpp index 737824f..3728c34 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,7 +32,7 @@ TaskHandle_t updateBoardTask; std::array*, 2> animations = { &RisingCubes::rising, - &RotatingCubes::rotating, + // &RotatingCubes::rotating, }; // BluetoothSerial SerialBT; @@ -112,6 +112,7 @@ void parseData(Message &message){ } case Commands::PING:{ GlobalPrint::Println("!" + String(Commands::PING) + ";"); + boardManager.PrintColorState(); break; } case Commands::SetStackColors:{ diff --git a/tools/animation-tools/.gitignore b/tools/animation-tools/.gitignore index 4ea05a1..77a7e05 100644 --- a/tools/animation-tools/.gitignore +++ b/tools/animation-tools/.gitignore @@ -1,2 +1,3 @@ venv/ -__pycache__/ \ No newline at end of file +__pycache__/ +output.txt \ No newline at end of file diff --git a/tools/animation-tools/AnimationExporter.py b/tools/animation-tools/AnimationExporter.py new file mode 100644 index 0000000..a267ad0 --- /dev/null +++ b/tools/animation-tools/AnimationExporter.py @@ -0,0 +1,74 @@ +from Scene import Scene +from enum import StrEnum +from pygame.math import Vector3 +from Shapes import Mesh + +class FillInterpolation(StrEnum): + NO_FILL = "NO_FILL" + CLOSEST_COLOR = "CLOSEST_COLOR" + LINEAR_WEIGHTED_DISTANCE = "LINEAR_WEIGHTED_DISTANCE" + SQUARE_WEIGHTED_DISTANCE = "SQUARE_WEIGHTED_DISTANCE" + +class FrameInterpolation(StrEnum): + SNAP = "SNAP" + FADE = "FADE" + +class Cell: + def __init__(self, position: Vector3, color: Vector3): + self.position: Vector3 = position + self.color: Vector3 = color + + def to_string(self): + message = f"CreateCell({self.position.x},{self.position.y},{self.position.z}" + message += f",V3D({self.color.x},{self.color.y},{self.color.z}))," + return message + +class AnimationFrame: + def __init__(self, cells: list[Cell], fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int): + self.cells = cells + self.fill = fill_interpolation + self.frame_interp = frame_interpolation + self.delay = delay + + def to_string(self, frame_number: int): + cell_str = "" + for cell in self.cells: + if cell.color.x != 0 or cell.color.y != 0 or cell.color.z != 0: + cell_str += cell.to_string() + "\n\t" + cell_str = cell_str[:-3] + message = f""" +AnimationFrame frame{frame_number}{{ + .frame = {{ + {cell_str} + }}, + .fillInterpolation = FillInterpolation::{self.fill}, + .frameInterpolation = FrameInterpolation::{self.frame_interp}, + .delay = std::chrono::milliseconds({self.delay}) +}}; + """ + return message + + +#TODO: Impliment this +def generate_animation_frame_struct(animation_types_path: str): + with open(animation_types_path, 'r') as AnimationTypes: + with open("AnimationTypes.py", 'w') as output: + pass + +def mesh_to_cell(mesh: Mesh) -> Cell: + if mesh.face_color[0] != 0 or mesh.face_color[1] != 0 or mesh.face_color[2] != 0: + pass + pos = (mesh.get_average_position() + Vector3(1,1,1)) / 2 + # need to swap z and y for the coordinate system + z = pos.y + pos.y = pos.z + pos.z = z + testVector = Vector3(0.1,0.5,0.9) + + cell = Cell(pos, Vector3(mesh.face_color)) + return cell + +def scene_to_frame(scene: Scene) -> str: + cells = [mesh_to_cell(cube) for cube in scene.meshes] + frame = AnimationFrame(cells, FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000) + return frame.to_string(0) \ No newline at end of file diff --git a/tools/animation-tools/README.md b/tools/animation-tools/README.md new file mode 100644 index 0000000..789b193 --- /dev/null +++ b/tools/animation-tools/README.md @@ -0,0 +1,31 @@ +# Setup +In order to run this tool follow these steps: + +1. ensure you're using python 3.12+. You can verify this by running +``` +python --version +``` +in a terminal. + +2. Inside of a terminal in the +``` +tools/animation-tools +``` +folder, run: +``` +python -m venv venv +``` + to create your virtual environment. + +3. Now activate the virtual environment. In powershell run +``` +venv\Scripts\activate +``` +You may need to fix permission issues if this is your first time using a virtual environment. For git bash just run: +``` +source venv/Scripts/activate +``` +4. Run +``` +pip install -r requirements.txt +``` \ No newline at end of file diff --git a/tools/animation-tools/Shapes.py b/tools/animation-tools/Shapes.py index 5280177..c72d6ca 100644 --- a/tools/animation-tools/Shapes.py +++ b/tools/animation-tools/Shapes.py @@ -18,6 +18,12 @@ class Mesh(): def calculate_average_z(self, vertices): return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)] + def get_average_position(self): + vertex_sum: Vector3 = Vector3() + for vertex in self.__vertices: + vertex_sum += vertex + return vertex_sum / len(self.__vertices) + def get_face(self, index): return self.__faces[index] def get_vertices(self): diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py index 4aad042..5a5b58f 100644 --- a/tools/animation-tools/animation-tool.py +++ b/tools/animation-tools/animation-tool.py @@ -4,6 +4,8 @@ import pygame_widgets from Shapes import * from Scene import Scene from CustomWidgets import ColorPicker +from pygame_widgets.button import Button +from AnimationExporter import scene_to_frame # generate a list of cubes @@ -17,10 +19,19 @@ for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): scene = Scene(meshes, 90, 5) +WINDOW_W = 500 +WINDOW_H = 500 + +def save_frame_to_file(frame_data=""): + frame_data += scene_to_frame(scene) + print(frame_data) + with open("tools/animation-tools/output.txt", 'w') as file: + file.write(frame_data) pygame.init() -window = pygame.display.set_mode((500, 500)) +window = pygame.display.set_mode((WINDOW_W, WINDOW_H)) colorPicker = ColorPicker(window, 20, 20, 100, 300) +save_button = Button(window, 20, WINDOW_H-40, 60, 30, text="Save",onClick=save_frame_to_file) clock = pygame.time.Clock() selected_meshes = [] run = True From 9d6c19cfd5537295f336985fe76978597c301334 Mon Sep 17 00:00:00 2001 From: Quinn Date: Tue, 27 Aug 2024 23:21:18 -0400 Subject: [PATCH 5/9] You can now generate many consecutive frames --- src/main.cpp | 3 +- tools/animation-tools/AnimationExporter.py | 4 +- tools/animation-tools/UI.py | 91 ++++++++++++++++++++++ tools/animation-tools/animation-tool.py | 50 +++--------- 4 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 tools/animation-tools/UI.py diff --git a/src/main.cpp b/src/main.cpp index 3728c34..91f810b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,7 +30,8 @@ TaskHandle_t updateCommunicaitonTask; TaskHandle_t updateBoardTask; -std::array*, 2> animations = { +// WARNING! This array size should always be equal to the number of entries in it!! +std::array*, 1> animations = { &RisingCubes::rising, // &RotatingCubes::rotating, }; diff --git a/tools/animation-tools/AnimationExporter.py b/tools/animation-tools/AnimationExporter.py index a267ad0..fdab606 100644 --- a/tools/animation-tools/AnimationExporter.py +++ b/tools/animation-tools/AnimationExporter.py @@ -68,7 +68,7 @@ def mesh_to_cell(mesh: Mesh) -> Cell: cell = Cell(pos, Vector3(mesh.face_color)) return cell -def scene_to_frame(scene: Scene) -> str: +def scene_to_frame(scene: Scene, scene_number: int) -> str: cells = [mesh_to_cell(cube) for cube in scene.meshes] frame = AnimationFrame(cells, FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000) - return frame.to_string(0) \ No newline at end of file + return frame.to_string(scene_number) \ No newline at end of file diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py new file mode 100644 index 0000000..29f5935 --- /dev/null +++ b/tools/animation-tools/UI.py @@ -0,0 +1,91 @@ +from AnimationExporter import scene_to_frame +from CustomWidgets import ColorPicker +from itertools import product +from pygame_widgets.button import Button +import pygame_widgets +from Scene import Scene +from Shapes import * +import pygame + +class SceneManager: + def __init__(self, window, color_picker: ColorPicker): + self.file_data: str = "" + self._scenes: list[Scene] = [self.new_scene()] + self._current_scene_idx: int = 0 + self.window = window + self.color_picker = color_picker + self._selected_meshes: list[Mesh] = [] + self.make_buttons() + + def make_buttons(self): + scr_wdt, scr_hgt = self.window.get_size() + self.save_button = Button(self.window, 20, scr_hgt-40, 60, 30, text="Save",onClick=self.save_frame_to_file) + self.next_frame_button = Button(self.window, scr_wdt-120, scr_hgt-40, 120, 30, text="Next Frame",onClick=self.next_scene) + self.last_frame_button = Button(self.window, scr_wdt-240, scr_hgt-40, 120, 30, text="last Frame",onClick=self.previous_scene) + def save_frame_to_file(self): + with open("tools/animation-tools/output.txt", 'w') as file: + for i, scene in enumerate(self._scenes): + file.write(scene_to_frame(scene, i)) + + def generate_meshes(self) -> list[Mesh]: + # generate a list of cubes + meshes: list[Mesh] = [] + for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): + cube = Cube() + cube.scale((0.35, 0.35, 0.35)) + cube.set_position(origin) + cube.set_face_color((0, 0, 0)) + meshes.append(cube) + return meshes + + def new_scene(self) -> Scene: + return Scene(self.generate_meshes(), 90, 5) + + def draw(self): + for mesh in self._selected_meshes: + mesh.set_face_color(self.color_picker.get_color()) + self.get_current_scene().draw(self.window) + + def get_current_scene(self) -> Scene: + return self._scenes[self._current_scene_idx] + + def click_mesh(self, coordinates: tuple[int, int]): + mesh = self.get_current_scene().get_mesh_from_xy(coordinates) + + if mesh == None: + return + + if mesh in self._selected_meshes: + mesh.set_edge_color((0,0,0)) + self._selected_meshes.remove(mesh) + else: + mesh.set_edge_color((255,0,0)) + self._selected_meshes.append(mesh) + + def deselect_all_mesh(self): + for mesh in self._selected_meshes: + mesh.set_edge_color((0,0,0)) + self._selected_meshes = [] + + def next_scene(self): + self.deselect_all_mesh() + current_angles = self.get_current_scene().euler_angles + if len(self._scenes)-1 == self._current_scene_idx: + self._scenes.append(self.new_scene()) + + self._current_scene_idx += 1 + + self.get_current_scene().euler_angles = [angle for angle in current_angles] + + def previous_scene(self): + if self._current_scene_idx > 0: + current_angles = self.get_current_scene().euler_angles + self._current_scene_idx -= 1 + self.get_current_scene().euler_angles = [angle for angle in current_angles] + self.deselect_all_mesh() + +def create_ui(window, scr_wdt, scr_hgt) -> SceneManager: + colorPicker = ColorPicker(window, 20, 20, 100, 300) + sceneManager = SceneManager(window, colorPicker) + + return sceneManager \ No newline at end of file diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py index 5a5b58f..cbcb2ab 100644 --- a/tools/animation-tools/animation-tool.py +++ b/tools/animation-tools/animation-tool.py @@ -1,37 +1,20 @@ import pygame -from itertools import product import pygame_widgets from Shapes import * from Scene import Scene -from CustomWidgets import ColorPicker -from pygame_widgets.button import Button -from AnimationExporter import scene_to_frame - - -# generate a list of cubes -meshes: list[Mesh] = [] -for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): - cube = Cube() - cube.scale((0.35, 0.35, 0.35)) - cube.set_position(origin) - cube.set_face_color((0, 0, 0)) - meshes.append(cube) - -scene = Scene(meshes, 90, 5) +from UI import create_ui, SceneManager WINDOW_W = 500 WINDOW_H = 500 -def save_frame_to_file(frame_data=""): - frame_data += scene_to_frame(scene) - print(frame_data) - with open("tools/animation-tools/output.txt", 'w') as file: - file.write(frame_data) + +file_data = "" pygame.init() window = pygame.display.set_mode((WINDOW_W, WINDOW_H)) -colorPicker = ColorPicker(window, 20, 20, 100, 300) -save_button = Button(window, 20, WINDOW_H-40, 60, 30, text="Save",onClick=save_frame_to_file) + +sceneManager: SceneManager = create_ui(window, WINDOW_W, WINDOW_H) + clock = pygame.time.Clock() selected_meshes = [] run = True @@ -40,6 +23,7 @@ trackMouseMotion = False while run: clock.tick(60) window.fill((255, 255, 255)) + current_scene: Scene = sceneManager.get_current_scene() events = pygame.event.get() for event in events: if event.type == pygame.QUIT: @@ -48,33 +32,21 @@ while run: if event.button == 2: # middle mouse click trackMouseMotion = True elif event.button == 1: # left click - mousePosition = pygame.mouse.get_pos() - mesh = scene.get_mesh_from_xy(mousePosition) - if mesh != None: - if mesh in selected_meshes: - mesh.set_edge_color((0,0,0)) - selected_meshes.remove(mesh) - else: - mesh.set_edge_color((255,0,0)) - selected_meshes.append(mesh) - - pass + sceneManager.click_mesh(pygame.mouse.get_pos()) elif event.type == pygame.MOUSEBUTTONUP: if event.button == 2: # middle mouse release trackMouseMotion = False elif trackMouseMotion and event.type == pygame.MOUSEMOTION: mouseMovement = pygame.mouse.get_rel() - scene.euler_angles[0] -= mouseMovement[1] - scene.euler_angles[1] -= mouseMovement[0] + current_scene.euler_angles[0] -= mouseMovement[1] + current_scene.euler_angles[1] -= mouseMovement[0] pygame_widgets.update(events) if not trackMouseMotion: pygame.mouse.get_rel() - for mesh in selected_meshes: - mesh.set_face_color(colorPicker.get_color()) - scene.draw(window) + sceneManager.draw() pygame.display.flip() pygame.quit() \ No newline at end of file From 5cb8101495b4dcdaa69311da755bdabf9526bec7 Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 28 Aug 2024 20:10:17 -0400 Subject: [PATCH 6/9] added a UI class to better organize UI creation --- tools/animation-tools/UI.py | 44 ++++++++++++++++++------- tools/animation-tools/animation-tool.py | 32 +++++------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py index 29f5935..5dca482 100644 --- a/tools/animation-tools/UI.py +++ b/tools/animation-tools/UI.py @@ -2,7 +2,6 @@ from AnimationExporter import scene_to_frame from CustomWidgets import ColorPicker from itertools import product from pygame_widgets.button import Button -import pygame_widgets from Scene import Scene from Shapes import * import pygame @@ -15,13 +14,7 @@ class SceneManager: self.window = window self.color_picker = color_picker self._selected_meshes: list[Mesh] = [] - self.make_buttons() - def make_buttons(self): - scr_wdt, scr_hgt = self.window.get_size() - self.save_button = Button(self.window, 20, scr_hgt-40, 60, 30, text="Save",onClick=self.save_frame_to_file) - self.next_frame_button = Button(self.window, scr_wdt-120, scr_hgt-40, 120, 30, text="Next Frame",onClick=self.next_scene) - self.last_frame_button = Button(self.window, scr_wdt-240, scr_hgt-40, 120, 30, text="last Frame",onClick=self.previous_scene) def save_frame_to_file(self): with open("tools/animation-tools/output.txt", 'w') as file: for i, scene in enumerate(self._scenes): @@ -83,9 +76,38 @@ class SceneManager: self._current_scene_idx -= 1 self.get_current_scene().euler_angles = [angle for angle in current_angles] self.deselect_all_mesh() + +class AnimatorUI: + def __init__(self, window): + scr_wdt, scr_hgt = window.get_size() -def create_ui(window, scr_wdt, scr_hgt) -> SceneManager: - colorPicker = ColorPicker(window, 20, 20, 100, 300) - sceneManager = SceneManager(window, colorPicker) + self.window = window + colorPicker: ColorPicker = ColorPicker(self.window, 20, 20, 100, 300) + self.sceneManager: SceneManager = SceneManager(window, colorPicker) + + + self.save_button: Button = Button(self.window, 20, scr_hgt-40, 60, 30, text="Save",onClick=self.sceneManager.save_frame_to_file) + self.next_frame_button: Button = Button(self.window, scr_wdt-120, scr_hgt-40, 120, 30, text="Next Frame",onClick=self.sceneManager.next_scene) + self.last_frame_button: Button = Button(self.window, scr_wdt-240, scr_hgt-40, 120, 30, text="last Frame",onClick=self.sceneManager.previous_scene) + self.trackMouseMotion: bool = False - return sceneManager \ No newline at end of file + def update_interaction(self, game_event): + if not self.trackMouseMotion: + pygame.mouse.get_rel() + + if game_event.type == pygame.MOUSEBUTTONDOWN: + if game_event.button == 2: # middle mouse click + self.trackMouseMotion = True + elif game_event.button == 1: # left click + self.sceneManager.click_mesh(pygame.mouse.get_pos()) + elif game_event.type == pygame.MOUSEBUTTONUP: + if game_event.button == 2: # middle mouse release + self.trackMouseMotion = False + elif self.trackMouseMotion and game_event.type == pygame.MOUSEMOTION: + mouseMovement = pygame.mouse.get_rel() + current_scene = self.sceneManager.get_current_scene() + current_scene.euler_angles[0] -= mouseMovement[1] + current_scene.euler_angles[1] -= mouseMovement[0] + + def draw(self): + self.sceneManager.draw() \ No newline at end of file diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py index cbcb2ab..8d683c3 100644 --- a/tools/animation-tools/animation-tool.py +++ b/tools/animation-tools/animation-tool.py @@ -2,7 +2,7 @@ import pygame import pygame_widgets from Shapes import * from Scene import Scene -from UI import create_ui, SceneManager +from UI import AnimatorUI WINDOW_W = 500 WINDOW_H = 500 @@ -13,40 +13,26 @@ file_data = "" pygame.init() window = pygame.display.set_mode((WINDOW_W, WINDOW_H)) -sceneManager: SceneManager = create_ui(window, WINDOW_W, WINDOW_H) +ui = AnimatorUI(window) clock = pygame.time.Clock() -selected_meshes = [] run = True -trackMouseMotion = False while run: clock.tick(60) window.fill((255, 255, 255)) - current_scene: Scene = sceneManager.get_current_scene() + events = pygame.event.get() for event in events: if event.type == pygame.QUIT: run = False - elif event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 2: # middle mouse click - trackMouseMotion = True - elif event.button == 1: # left click - sceneManager.click_mesh(pygame.mouse.get_pos()) - elif event.type == pygame.MOUSEBUTTONUP: - if event.button == 2: # middle mouse release - trackMouseMotion = False - elif trackMouseMotion and event.type == pygame.MOUSEMOTION: - mouseMovement = pygame.mouse.get_rel() - current_scene.euler_angles[0] -= mouseMovement[1] - current_scene.euler_angles[1] -= mouseMovement[0] - pygame_widgets.update(events) - - - if not trackMouseMotion: - pygame.mouse.get_rel() + # if the event isn't handled above as a global event, let the ui handle it + else: + ui.update_interaction(event) - sceneManager.draw() + pygame_widgets.update(events) + + ui.draw() pygame.display.flip() pygame.quit() \ No newline at end of file From 12193dc560820d29ac0ea9b59dcb1942a9dce91e Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 28 Aug 2024 22:16:11 -0400 Subject: [PATCH 7/9] added dropdown menus --- lib/Animator/Animation.h | 35 ++++-- tools/animation-tools/AnimationExporter.py | 4 +- tools/animation-tools/CustomWidgets.py | 18 --- tools/animation-tools/UI.py | 129 +++++++++++++++++---- tools/animation-tools/animation-tool.py | 1 - 5 files changed, 136 insertions(+), 51 deletions(-) delete mode 100644 tools/animation-tools/CustomWidgets.py diff --git a/lib/Animator/Animation.h b/lib/Animator/Animation.h index 6f78b7a..189877c 100644 --- a/lib/Animator/Animation.h +++ b/lib/Animator/Animation.h @@ -211,23 +211,38 @@ namespace RisingCubes{ .delay = std::chrono::milliseconds(1) }; - AnimationFrame frame0{ + AnimationFrame frame00{ .frame = { - CreateCell(0.0,0.0,0.0,V3D(255.0,255.0,255.0)), - CreateCell(0.0,0.5,0.0,V3D(0.0,255.0,0.0)), - CreateCell(0.0,1.0,0.0,V3D(0.0,255.0,0.0)), - CreateCell(0.0,0.0,0.5,V3D(0.0,0.0,255.0)), - CreateCell(0.0,0.0,1.0,V3D(0.0,0.0,255.0)), - CreateCell(0.5,0.0,0.0,V3D(255.0,0.0,0.0)), - CreateCell(1.0,0.0,0.0,V3D(255.0,0.0,0.0)) + CreateCell(0.0,0.0,1.0,V3D(128.0,128.0,255.0)), + CreateCell(0.5,0.0,1.0,V3D(128.0,128.0,255.0)), + CreateCell(1.0,0.0,0.0,V3D(255.0,118.0,205.0)), + CreateCell(1.0,0.0,0.5,V3D(255.0,118.0,205.0)), + CreateCell(1.0,0.0,1.0,V3D(255.0,116.0,0.0)), + CreateCell(1.0,0.5,1.0,V3D(183.0,0.0,255.0)), + CreateCell(1.0,1.0,1.0,V3D(183.0,0.0,255.0)) }, - .fillInterpolation = FillInterpolation::NO_FILL, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame01{ + .frame = { + CreateCell(0.0,0.0,1.0,V3D(255.0,255.0,171.0)), + CreateCell(0.5,0.0,1.0,V3D(255.0,255.0,171.0)), + CreateCell(1.0,0.0,0.0,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.0,0.5,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.0,1.0,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.5,1.0,V3D(112.0,222.0,255.0)), + CreateCell(1.0,1.0,1.0,V3D(112.0,222.0,255.0)) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, .frameInterpolation = FrameInterpolation::FADE, .delay = std::chrono::milliseconds(1000) }; std::vector rising{ - frame0, frame0 + frame00, frame01, frame00 // frame1, // 0 // frame2, // 1 // frame3, // 2 diff --git a/tools/animation-tools/AnimationExporter.py b/tools/animation-tools/AnimationExporter.py index fdab606..db194b5 100644 --- a/tools/animation-tools/AnimationExporter.py +++ b/tools/animation-tools/AnimationExporter.py @@ -8,10 +8,12 @@ class FillInterpolation(StrEnum): CLOSEST_COLOR = "CLOSEST_COLOR" LINEAR_WEIGHTED_DISTANCE = "LINEAR_WEIGHTED_DISTANCE" SQUARE_WEIGHTED_DISTANCE = "SQUARE_WEIGHTED_DISTANCE" + # options = [NO_FILL, CLOSEST_COLOR, LINEAR_WEIGHTED_DISTANCE, SQUARE_WEIGHTED_DISTANCE] class FrameInterpolation(StrEnum): SNAP = "SNAP" FADE = "FADE" + # options = [SNAP, FADE] class Cell: def __init__(self, position: Vector3, color: Vector3): @@ -68,7 +70,7 @@ def mesh_to_cell(mesh: Mesh) -> Cell: cell = Cell(pos, Vector3(mesh.face_color)) return cell -def scene_to_frame(scene: Scene, scene_number: int) -> str: +def scene_to_frame(scene: Scene, fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int, scene_number: int) -> str: cells = [mesh_to_cell(cube) for cube in scene.meshes] frame = AnimationFrame(cells, FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000) return frame.to_string(scene_number) \ No newline at end of file diff --git a/tools/animation-tools/CustomWidgets.py b/tools/animation-tools/CustomWidgets.py deleted file mode 100644 index 395baa8..0000000 --- a/tools/animation-tools/CustomWidgets.py +++ /dev/null @@ -1,18 +0,0 @@ -from pygame_widgets.slider import Slider - -class ColorPicker: - def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): - width = max(50, width) - self.sliders: list[Slider] = [ - Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True) for i in range(3) - ] - for slider in self.sliders: - slider.enable() - - def get_color(self) -> tuple[int]: - # return (0,0,0) - return tuple([slider.getValue() for slider in self.sliders]) - - def set_color(self, color: tuple[int]): - for i, slider in enumerate(self.sliders): - slider.setValue(color[i]) \ No newline at end of file diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py index 5dca482..0651c96 100644 --- a/tools/animation-tools/UI.py +++ b/tools/animation-tools/UI.py @@ -1,15 +1,41 @@ -from AnimationExporter import scene_to_frame -from CustomWidgets import ColorPicker from itertools import product from pygame_widgets.button import Button +from pygame_widgets.slider import Slider +from pygame_widgets.dropdown import Dropdown +from pygame_widgets.textbox import TextBox from Scene import Scene from Shapes import * import pygame +from AnimationExporter import * +class ColorPicker: + def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): + width = max(50, width) + slider_colors = ((255,0,0),(0,255,0),(0,0,255)) + self.sliders: list[Slider] = [ + Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True, handleColour=slider_colors[i]) for i in range(3) + ] + for slider in self.sliders: + slider.enable() + + def get_color(self) -> tuple[int]: + return tuple([slider.getValue() for slider in self.sliders]) + + def set_color(self, color: tuple[int]): + for i, slider in enumerate(self.sliders): + slider.setValue(color[i]) + +class SceneStore: + def __init__(self, scene: Scene, fill: FillInterpolation, fade: FrameInterpolation, delay: int): + self.scene: Scene = scene + self.fill: FillInterpolation = fill + self.fade: FrameInterpolation = fade + self.delay: int = delay + class SceneManager: def __init__(self, window, color_picker: ColorPicker): self.file_data: str = "" - self._scenes: list[Scene] = [self.new_scene()] + self._scenes: list[SceneStore] = [SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000)] self._current_scene_idx: int = 0 self.window = window self.color_picker = color_picker @@ -18,7 +44,7 @@ class SceneManager: def save_frame_to_file(self): with open("tools/animation-tools/output.txt", 'w') as file: for i, scene in enumerate(self._scenes): - file.write(scene_to_frame(scene, i)) + file.write(scene_to_frame(scene.scene, scene.fill_interpolation, scene.frame_interpolation, scene.delay, i)) def generate_meshes(self) -> list[Mesh]: # generate a list of cubes @@ -37,13 +63,13 @@ class SceneManager: def draw(self): for mesh in self._selected_meshes: mesh.set_face_color(self.color_picker.get_color()) - self.get_current_scene().draw(self.window) + self.get_current_scene().scene.draw(self.window) - def get_current_scene(self) -> Scene: + def get_current_scene(self) -> SceneStore: return self._scenes[self._current_scene_idx] def click_mesh(self, coordinates: tuple[int, int]): - mesh = self.get_current_scene().get_mesh_from_xy(coordinates) + mesh = self.get_current_scene().scene.get_mesh_from_xy(coordinates) if mesh == None: return @@ -62,34 +88,60 @@ class SceneManager: def next_scene(self): self.deselect_all_mesh() - current_angles = self.get_current_scene().euler_angles + current_angles = self.get_current_scene().scene.euler_angles if len(self._scenes)-1 == self._current_scene_idx: - self._scenes.append(self.new_scene()) + self._scenes.append(SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000)) self._current_scene_idx += 1 - self.get_current_scene().euler_angles = [angle for angle in current_angles] + self.get_current_scene().scene.euler_angles = [angle for angle in current_angles] - def previous_scene(self): + def last_scene(self): if self._current_scene_idx > 0: - current_angles = self.get_current_scene().euler_angles + current_angles = self.get_current_scene().scene.euler_angles self._current_scene_idx -= 1 - self.get_current_scene().euler_angles = [angle for angle in current_angles] + self.get_current_scene().scene.euler_angles = [angle for angle in current_angles] self.deselect_all_mesh() - + + def update_scene_options(self, fill: FillInterpolation, fade: FrameInterpolation, delay: int): + cur_scene: SceneStore = self.get_current_scene() + cur_scene.fill = fill + cur_scene.fade = fade + cur_scene.delay = delay + class AnimatorUI: def __init__(self, window): scr_wdt, scr_hgt = window.get_size() self.window = window - colorPicker: ColorPicker = ColorPicker(self.window, 20, 20, 100, 300) - self.sceneManager: SceneManager = SceneManager(window, colorPicker) + self.colorPicker: ColorPicker = ColorPicker(self.window, *self.rel2abs(5, 5, 20, 60)) + self.sceneManager: SceneManager = SceneManager(window, self.colorPicker) - - self.save_button: Button = Button(self.window, 20, scr_hgt-40, 60, 30, text="Save",onClick=self.sceneManager.save_frame_to_file) - self.next_frame_button: Button = Button(self.window, scr_wdt-120, scr_hgt-40, 120, 30, text="Next Frame",onClick=self.sceneManager.next_scene) - self.last_frame_button: Button = Button(self.window, scr_wdt-240, scr_hgt-40, 120, 30, text="last Frame",onClick=self.sceneManager.previous_scene) + self.save_button: Button = Button(self.window, *self.rel2abs(5, 90, 10, 10), text="Save",onClick=self.sceneManager.save_frame_to_file) + self.next_frame_button: Button = Button(self.window, *self.rel2abs(75, 90, 25, 5), text="Next Frame",onClick=self._next_scene) + self.last_frame_button: Button = Button(self.window, *self.rel2abs(50, 90, 25, 5), text="last Frame",onClick=self._last_scene) self.trackMouseMotion: bool = False + + self.fill_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(30, 0, 40, 5), name="Fill Type", + choices=[option.value for option in FillInterpolation], onRelease=self.set_update_options_flag) + + self.fade_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(70, 0, 20, 5), name="Fade Type", + choices=[option.value for option in FrameInterpolation], onRelease=self.set_update_options_flag) + + self.updateOptions = False + self._set_dropdown_options() + + # TODO: add delay field + # TODO: Make a frame counter + + def rel2abs(self, x: float, y: float, width: float, height: float) -> tuple[int,int,int,int]: + scr_wdt, scr_hgt = self.window.get_size() + x_abs = int(x*scr_wdt/100) + y_abs = int(y*scr_hgt/100) + w_abs = int(width*scr_wdt/100) + h_abs = int(height*scr_hgt/100) + return (x_abs, y_abs, w_abs, h_abs) + def update_interaction(self, game_event): if not self.trackMouseMotion: @@ -110,4 +162,39 @@ class AnimatorUI: current_scene.euler_angles[1] -= mouseMovement[0] def draw(self): - self.sceneManager.draw() \ No newline at end of file + if self.updateOptions: + self.set_scene_options() + # make the preview window for the color picker + pygame.draw.rect(self.window, self.colorPicker.get_color(), self.rel2abs(11, 70, 5, 5)) + pygame.draw.rect(self.window, (0,0,0), self.rel2abs(11, 70, 5, 5), 2) + + self.sceneManager.draw() + + def _next_scene(self): + self.sceneManager.next_scene() + self._set_dropdown_options() + + + def _last_scene(self): + self.sceneManager.last_scene() + self._set_dropdown_options() + + def _set_dropdown_options(self): + scene = self.sceneManager.get_current_scene() + for choice in self.fill_dropdown._Dropdown__choices: + if choice.text.find(scene.fill) != -1: + self.fill_dropdown.chosen = choice + break + + for choice in self.fade_dropdown._Dropdown__choices: + if choice.text.find(scene.fade) != -1: + self.fade_dropdown.chosen = choice + break + + def set_update_options_flag(self): + self.updateOptions = True + + def set_scene_options(self): + self.updateOptions = False + self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), 1000) + current_scene = self.sceneManager.get_current_scene() \ No newline at end of file diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py index 8d683c3..b642227 100644 --- a/tools/animation-tools/animation-tool.py +++ b/tools/animation-tools/animation-tool.py @@ -1,7 +1,6 @@ import pygame import pygame_widgets from Shapes import * -from Scene import Scene from UI import AnimatorUI WINDOW_W = 500 From ecd516bc6f00d3b3ba19a38f9b1921d4be35ade5 Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 28 Aug 2024 22:33:50 -0400 Subject: [PATCH 8/9] Added a frame counter --- tools/animation-tools/UI.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py index 0651c96..e3ce2b2 100644 --- a/tools/animation-tools/UI.py +++ b/tools/animation-tools/UI.py @@ -132,7 +132,9 @@ class AnimatorUI: self._set_dropdown_options() # TODO: add delay field - # TODO: Make a frame counter + # Make a frame counter as a button but make it not look like a button + default_color=(150,150,150) + self.frame_counter_text: Button = Button(self.window, *self.rel2abs(15, 90, 10, 10), text="0", inactiveColour=default_color, hoverColour=default_color, pressedColour=default_color) def rel2abs(self, x: float, y: float, width: float, height: float) -> tuple[int,int,int,int]: scr_wdt, scr_hgt = self.window.get_size() @@ -173,13 +175,16 @@ class AnimatorUI: def _next_scene(self): self.sceneManager.next_scene() self._set_dropdown_options() + self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx)) def _last_scene(self): self.sceneManager.last_scene() self._set_dropdown_options() + self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx)) def _set_dropdown_options(self): + # cursed method to set dropdown options because for some reason pygame_widgets dropdown doesn't allow you to manually set the dropdown option scene = self.sceneManager.get_current_scene() for choice in self.fill_dropdown._Dropdown__choices: if choice.text.find(scene.fill) != -1: @@ -196,5 +201,4 @@ class AnimatorUI: def set_scene_options(self): self.updateOptions = False - self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), 1000) - current_scene = self.sceneManager.get_current_scene() \ No newline at end of file + self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), 1000) \ No newline at end of file From 2f8469ef6cce2713a31c226555bbaaa40c86754a Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 28 Aug 2024 22:55:52 -0400 Subject: [PATCH 9/9] finished the animator tool's UI --- tools/animation-tools/AnimationExporter.py | 6 ++++-- tools/animation-tools/UI.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/animation-tools/AnimationExporter.py b/tools/animation-tools/AnimationExporter.py index db194b5..19246b1 100644 --- a/tools/animation-tools/AnimationExporter.py +++ b/tools/animation-tools/AnimationExporter.py @@ -34,8 +34,10 @@ class AnimationFrame: def to_string(self, frame_number: int): cell_str = "" - for cell in self.cells: + for i, cell in enumerate(self.cells): if cell.color.x != 0 or cell.color.y != 0 or cell.color.z != 0: + if i != 0: + cell_str += "\t\t" cell_str += cell.to_string() + "\n\t" cell_str = cell_str[:-3] message = f""" @@ -72,5 +74,5 @@ def mesh_to_cell(mesh: Mesh) -> Cell: def scene_to_frame(scene: Scene, fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int, scene_number: int) -> str: cells = [mesh_to_cell(cube) for cube in scene.meshes] - frame = AnimationFrame(cells, FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000) + frame = AnimationFrame(cells, fill_interpolation, frame_interpolation, delay) return frame.to_string(scene_number) \ No newline at end of file diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py index e3ce2b2..8426aab 100644 --- a/tools/animation-tools/UI.py +++ b/tools/animation-tools/UI.py @@ -44,7 +44,7 @@ class SceneManager: def save_frame_to_file(self): with open("tools/animation-tools/output.txt", 'w') as file: for i, scene in enumerate(self._scenes): - file.write(scene_to_frame(scene.scene, scene.fill_interpolation, scene.frame_interpolation, scene.delay, i)) + file.write(scene_to_frame(scene.scene, scene.fill, scene.fade, scene.delay, i)) def generate_meshes(self) -> list[Mesh]: # generate a list of cubes @@ -131,7 +131,8 @@ class AnimatorUI: self.updateOptions = False self._set_dropdown_options() - # TODO: add delay field + self.delay_text_entry: TextBox = TextBox(self.window, *self.rel2abs(30, 5, 60, 7), placeholderText="Transition Time (ms)", onSubmit=self.set_update_options_flag) + # Make a frame counter as a button but make it not look like a button default_color=(150,150,150) self.frame_counter_text: Button = Button(self.window, *self.rel2abs(15, 90, 10, 10), text="0", inactiveColour=default_color, hoverColour=default_color, pressedColour=default_color) @@ -201,4 +202,9 @@ class AnimatorUI: def set_scene_options(self): self.updateOptions = False - self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), 1000) \ No newline at end of file + delay_time = 1000 + try: + delay_time = int(self.delay_text_entry.getText()) + except ValueError: + self.delay_text_entry.setText(str(delay_time)) + self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), delay_time) \ No newline at end of file