diff --git a/editor-blender/__init__.py b/editor-blender/__init__.py index b11e2a592..4c9b0cab4 100644 --- a/editor-blender/__init__.py +++ b/editor-blender/__init__.py @@ -17,14 +17,10 @@ def setup(): # Ensure requirements are installed (for release) install_requirements() - from os import path + # Initialize constants + from .core.constants import constants - from dotenv import load_dotenv - - # Load .env - root_dir = path.dirname(path.realpath(__file__)) - dotenv_path = path.join(root_dir, ".env") - load_dotenv(dotenv_path=dotenv_path) + constants.initialize() def register(): diff --git a/editor-blender/client/__init__.py b/editor-blender/client/__init__.py index d71b217b8..f70b77f49 100644 --- a/editor-blender/client/__init__.py +++ b/editor-blender/client/__init__.py @@ -12,6 +12,7 @@ from gql.transport.websockets import WebsocketsTransport from graphql import DocumentNode +from ..core.constants import constants from ..core.states import state from .cache import InMemoryCache, query_defs_to_field_table @@ -50,30 +51,30 @@ def remove_wrapped_slash(path: str) -> str: class Clients: def __init__(self): - SERVER_URL = os.getenv("SERVER_URL") - if SERVER_URL is None: - raise Exception("SERVER_URL is not defined") - self.SERVER_URL = remove_wrapped_slash(SERVER_URL) - - HTTP_PATH = os.getenv("HTTP_PATH") - if HTTP_PATH is None: - raise Exception("HTTP_PATH is not defined") - self.HTTP_PATH = remove_wrapped_slash(HTTP_PATH) - - GRAPHQL_PATH = os.getenv("GRAPHQL_PATH") - if GRAPHQL_PATH is None: - raise Exception("GRAPHQL_PATH is not defined") - self.GRAPHQL_PATH = remove_wrapped_slash(GRAPHQL_PATH) - - GRAPHQL_WS_PATH = os.getenv("GRAPHQL_WS_PATH") - if GRAPHQL_WS_PATH is None: - raise Exception("GRAPHQL_WS_PATH is not defined") - self.GRAPHQL_WS_PATH = remove_wrapped_slash(GRAPHQL_WS_PATH) - - FILE_SERVER_URL = os.getenv("FILE_SERVER_URL") - if FILE_SERVER_URL is None: - raise Exception("FILE_SERVER_URL is not defined") - self.FILE_SERVER_URL = remove_wrapped_slash(FILE_SERVER_URL) + # SERVER_URL = os.getenv("SERVER_URL") + # if SERVER_URL is None: + # raise Exception("SERVER_URL is not defined") + # self.SERVER_URL = remove_wrapped_slash(SERVER_URL) + # + # HTTP_PATH = os.getenv("HTTP_PATH") + # if HTTP_PATH is None: + # raise Exception("HTTP_PATH is not defined") + # self.HTTP_PATH = remove_wrapped_slash(HTTP_PATH) + # + # GRAPHQL_PATH = os.getenv("GRAPHQL_PATH") + # if GRAPHQL_PATH is None: + # raise Exception("GRAPHQL_PATH is not defined") + # self.GRAPHQL_PATH = remove_wrapped_slash(GRAPHQL_PATH) + # + # GRAPHQL_WS_PATH = os.getenv("GRAPHQL_WS_PATH") + # if GRAPHQL_WS_PATH is None: + # raise Exception("GRAPHQL_WS_PATH is not defined") + # self.GRAPHQL_WS_PATH = remove_wrapped_slash(GRAPHQL_WS_PATH) + # + # FILE_SERVER_URL = os.getenv("FILE_SERVER_URL") + # if FILE_SERVER_URL is None: + # raise Exception("FILE_SERVER_URL is not defined") + # self.FILE_SERVER_URL = remove_wrapped_slash(FILE_SERVER_URL) self.http_client: Optional[ClientSession] = None self.client: Optional[GQLSession] = None @@ -95,7 +96,7 @@ async def __post__(self, path: str, json: Optional[Any] = None) -> Any: raise Exception("HTTP client is not initialized") path = remove_wrapped_slash(path) - http_path = f"/{self.HTTP_PATH}/{path}" + http_path = f"/{constants.HTTP_PATH}/{path}" async with self.http_client.post(http_path, json=json) as response: return await response.json() @@ -104,7 +105,7 @@ async def __get__(self, path: str) -> Any: raise Exception("HTTP client is not initialized") path = remove_wrapped_slash(path) - http_path = f"/{self.HTTP_PATH}/{path}" + http_path = f"/{constants.HTTP_PATH}/{path}" async with self.http_client.get(http_path) as response: return await response.json() @@ -226,7 +227,7 @@ async def open_http(self) -> None: token_payload = {"token": state.token} # HTTP client - self.http_client = ClientSession(self.SERVER_URL, cookies=token_payload) + self.http_client = ClientSession(constants.SERVER_URL, cookies=token_payload) print("HTTP client opened") async def close_http(self) -> None: @@ -239,7 +240,7 @@ async def restart_http(self) -> None: async def open_file(self) -> None: # File client - self.file_client = ClientSession(self.FILE_SERVER_URL) + self.file_client = ClientSession(constants.FILE_SERVER_URL) print("File client opened") async def close_file(self) -> None: @@ -257,7 +258,8 @@ async def open_graphql(self) -> None: # GraphQL client transport = AIOHTTPTransport( - url=f"{self.SERVER_URL}/{self.GRAPHQL_PATH}", cookies=token_payload + url=f"{constants.SERVER_URL}/{constants.GRAPHQL_PATH}", + cookies=token_payload, ) self.client = await Client( @@ -266,9 +268,9 @@ async def open_graphql(self) -> None: print("GraphQL client opened") # GraphQL subscription client - ws_url = self.SERVER_URL.replace("http", "ws") + ws_url = constants.SERVER_URL.replace("http", "ws") sub_transport = WebsocketsTransport( - url=f"{ws_url}/{self.GRAPHQL_WS_PATH}", + url=f"{ws_url}/{constants.GRAPHQL_WS_PATH}", subprotocols=[WebsocketsTransport.GRAPHQLWS_SUBPROTOCOL], init_payload=token_payload, ) diff --git a/editor-blender/core/actions/state/control_map.py b/editor-blender/core/actions/state/control_map.py index 6fb9596d0..d3a016cf9 100644 --- a/editor-blender/core/actions/state/control_map.py +++ b/editor-blender/core/actions/state/control_map.py @@ -92,15 +92,24 @@ def apply_control_map_updates(): control_map_updates = state.control_map_updates for status in control_map_updates.added: - add_single_ctrl_keyframe(status[0], status[1]) + try: + add_single_ctrl_keyframe(status[0], status[1]) + except Exception as e: + notify("ERROR", f"Failed to add control keyframe {status[0]}: {e}") state.control_map[status[0]] = status[1] for status in control_map_updates.updated: - edit_single_ctrl_keyframe(status[0], status[1]) + try: + edit_single_ctrl_keyframe(status[0], status[1]) + except Exception as e: + notify("ERROR", f"Failed to update control keyframe {status[0]}: {e}") state.control_map[status[0]] = status[1] for id in control_map_updates.deleted: - delete_single_ctrl_keyframe(id) + try: + delete_single_ctrl_keyframe(id) + except Exception as e: + notify("ERROR", f"Failed to delete control keyframe {id}: {e}") del state.control_map[id] control_map_updates.added.clear() diff --git a/editor-blender/core/actions/state/pos_map.py b/editor-blender/core/actions/state/pos_map.py index 4c0e5b44f..7d67d2e41 100644 --- a/editor-blender/core/actions/state/pos_map.py +++ b/editor-blender/core/actions/state/pos_map.py @@ -89,15 +89,24 @@ def apply_pos_map_updates(): pos_map_updates = state.pos_map_updates for pos in pos_map_updates.added: - add_single_pos_keyframe(pos[0], pos[1]) + try: + add_single_pos_keyframe(pos[0], pos[1]) + except Exception as e: + notify("ERROR", f"Failed to add position keyframe {pos[0]}: {e}") state.pos_map[pos[0]] = pos[1] for pos in pos_map_updates.updated: - edit_single_pos_keyframe(pos[0], pos[1]) + try: + edit_single_pos_keyframe(pos[0], pos[1]) + except Exception as e: + notify("ERROR", f"Failed to edit position keyframe {pos[0]}: {e}") state.pos_map[pos[0]] = pos[1] for pos_id in pos_map_updates.deleted: - delete_single_pos_keyframe(pos_id) + try: + delete_single_pos_keyframe(pos_id) + except Exception as e: + notify("ERROR", f"Failed to delete position keyframe {pos_id}: {e}") del state.pos_map[pos_id] pos_map_updates.added.clear() diff --git a/editor-blender/core/constants/__init__.py b/editor-blender/core/constants/__init__.py new file mode 100644 index 000000000..711cda120 --- /dev/null +++ b/editor-blender/core/constants/__init__.py @@ -0,0 +1,62 @@ +import os +from os import path +from typing import cast + +import bpy +from dotenv import load_dotenv + + +def remove_wrapped_slash(path: str) -> str: + if path.startswith("/"): + return path[1:] + return path + + +class Constants: + def __init__(self): + pass + + def initialize(self): + """ + Dotenv + """ + current_dir = path.dirname(path.realpath(__file__)) + root_dir = path.dirname(path.dirname(current_dir)) + dotenv_path = path.join(root_dir, ".env") + load_dotenv(dotenv_path=dotenv_path) + + SERVER_URL = os.getenv("SERVER_URL") + if SERVER_URL is None: + raise Exception("SERVER_URL is not defined") + self.SERVER_URL = remove_wrapped_slash(SERVER_URL) + + HTTP_PATH = os.getenv("HTTP_PATH") + if HTTP_PATH is None: + raise Exception("HTTP_PATH is not defined") + self.HTTP_PATH = remove_wrapped_slash(HTTP_PATH) + + GRAPHQL_PATH = os.getenv("GRAPHQL_PATH") + if GRAPHQL_PATH is None: + raise Exception("GRAPHQL_PATH is not defined") + self.GRAPHQL_PATH = remove_wrapped_slash(GRAPHQL_PATH) + + GRAPHQL_WS_PATH = os.getenv("GRAPHQL_WS_PATH") + if GRAPHQL_WS_PATH is None: + raise Exception("GRAPHQL_WS_PATH is not defined") + self.GRAPHQL_WS_PATH = remove_wrapped_slash(GRAPHQL_WS_PATH) + + FILE_SERVER_URL = os.getenv("FILE_SERVER_URL") + if FILE_SERVER_URL is None: + raise Exception("FILE_SERVER_URL is not defined") + self.FILE_SERVER_URL = remove_wrapped_slash(FILE_SERVER_URL) + + """ + Assets + """ + library_path = cast( + str, bpy.context.preferences.filepaths.asset_libraries["User Library"].path + ) + self.ASSET_PATH = os.path.join(library_path, "LightDance") + + +constants = Constants() diff --git a/editor-blender/handlers/waveform.py b/editor-blender/handlers/waveform.py index e5a2caa43..3926bdbda 100644 --- a/editor-blender/handlers/waveform.py +++ b/editor-blender/handlers/waveform.py @@ -7,6 +7,7 @@ import gpu from gpu_extras import batch as g_batch +from ..core.constants import constants from ..core.states import state from ..core.utils.ui import redraw_area @@ -60,7 +61,7 @@ def mount(): global waveform_settings # Load waveform data - waveform_path = os.path.join(state.assets_path, "data/waveform.json") + waveform_path = os.path.join(constants.ASSET_PATH, "data/waveform.json") try: waveform_file = open(waveform_path, "r") diff --git a/editor-blender/operators/__init__.py b/editor-blender/operators/__init__.py index 20c435780..b20b3d00f 100644 --- a/editor-blender/operators/__init__.py +++ b/editor-blender/operators/__init__.py @@ -1,5 +1,6 @@ from . import ( animation, + assets, async_core, auth, color_palette, @@ -31,6 +32,7 @@ def register(): control_editor.register() led_editor.register() shift.register() + assets.register() def unregister(): @@ -48,3 +50,4 @@ def unregister(): control_editor.unregister() led_editor.unregister() shift.unregister() + assets.unregister() diff --git a/editor-blender/operators/assets/__init__.py b/editor-blender/operators/assets/__init__.py new file mode 100644 index 000000000..be9bfa680 --- /dev/null +++ b/editor-blender/operators/assets/__init__.py @@ -0,0 +1,49 @@ +import os + +import bpy + +from ...core.constants import constants +from ...core.utils.notification import notify + + +def remove_dir_files(dir_path: str): + for content in os.listdir(dir_path): + content_path = os.path.join(dir_path, content) + if os.path.isdir(content_path): + remove_dir_files(content_path) + else: + os.remove(content_path) + + +class ClearAssets(bpy.types.Operator): + bl_idname = "lightdance.clear_assets" + bl_label = "Clear Assets" + bl_options = {"REGISTER", "UNDO"} + + confirm: bpy.props.BoolProperty( # type: ignore + name="I know what I am doing", + default=False, + ) + + def execute(self, context: bpy.types.Context): + confirm: bool = getattr(self, "confirm") + + if not confirm: + notify("ERROR", "Cancelled") + return {"CANCELLED"} + + remove_dir_files(constants.ASSET_PATH) + notify("INFO", "Assets cleared") + + return {"FINISHED"} + + def invoke(self, context: bpy.types.Context, event: bpy.types.Event): + return context.window_manager.invoke_props_dialog(self) + + +def register(): + bpy.utils.register_class(ClearAssets) + + +def unregister(): + bpy.utils.unregister_class(ClearAssets) diff --git a/editor-blender/panels/lightdance/__init__.py b/editor-blender/panels/lightdance/__init__.py index 779f3ca3a..16d5457b6 100644 --- a/editor-blender/panels/lightdance/__init__.py +++ b/editor-blender/panels/lightdance/__init__.py @@ -37,6 +37,8 @@ def draw(self, context: bpy.types.Context): row = layout.row() row.operator("lightdance.toggle_shifting", text="Timeshift", icon="PLAY") row = layout.row() + row.operator("lightdance.clear_assets", text="Clear Assets", icon="PLAY") + row = layout.row() row.operator("lightdance.logout", text="Logout", icon="PLAY")