diff --git a/pyproject.toml b/pyproject.toml index 05aa40a..213d78d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "pyperclip==1.9.0", "watchdog==5.0.3", "requests==2.32.3", - "nicegui==2.3.0", + "nicegui==2.5.0", "librosa==0.10.2.post1", "soundfile==0.12.1", "pywin32==308; sys_platform=='win32'", diff --git a/src/synth_mapping_helper/__init__.py b/src/synth_mapping_helper/__init__.py index 2ef9031..7a1025f 100644 --- a/src/synth_mapping_helper/__init__.py +++ b/src/synth_mapping_helper/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.2" +__version__ = "1.5.3" from . import movement from . import pattern_generation diff --git a/src/synth_mapping_helper/gui_tabs/map_render.py b/src/synth_mapping_helper/gui_tabs/map_render.py index 52fe404..51afd89 100644 --- a/src/synth_mapping_helper/gui_tabs/map_render.py +++ b/src/synth_mapping_helper/gui_tabs/map_render.py @@ -44,6 +44,7 @@ class SettingsPanel(ui.element): def __init__(self) -> None: super().__init__() with self: + ui.label("Note: These settings only affect the preview.").tooltip("The game does not allow maps to override colors.") with ui.row(): self.wall_size = make_input("Wall Depth", pretty_fraction(DEFAULT_SETTINGS.wall.size), "wall_size", suffix="b") ui.separator().props("vertical") diff --git a/src/synth_mapping_helper/gui_tabs/stacking.py b/src/synth_mapping_helper/gui_tabs/stacking.py index a14a47c..15a5419 100644 --- a/src/synth_mapping_helper/gui_tabs/stacking.py +++ b/src/synth_mapping_helper/gui_tabs/stacking.py @@ -434,16 +434,17 @@ def _soft_refresh(): ui.label("Clipboard Preview").classes("my-auto") with ui.button(icon="sync", on_click=_soft_refresh, color="positive").props("outline"): ui.tooltip("Preview current clipboard") - with ui.expansion("Settings", icon="settings").props("dense"): - with ui.row(): - scene_width = make_input("Width", "800", "width", tab_id="preview", suffix="px", tooltip="Width of the preview in px") - scene_height = make_input("Height", "600", "height", tab_id="preview", suffix="px", tooltip="Height of the preview in px") + with ui.expansion("Preview Settings", icon="palette").props("dense"): + sp = SettingsPanel() + ui.separator() with ui.row(): - time_scale = make_input("Time Scale", "64", "time_scale", tab_id="preview", tooltip="Ratio between XY and time") - frame_length = make_input("Frame Length", "16", "frame_length", tab_id="preview", suffix="b", tooltip="Number of beats to draw frames for") + ui.icon("preview", size="3em").tooltip("Change size and scaling of preview") + scene_width = make_input("Width", "800", "width", tab_id="preview", suffix="px", tooltip="Width of the preview in px", width=20) + scene_height = make_input("Height", "600", "height", tab_id="preview", suffix="px", tooltip="Height of the preview in px", width=20) + time_scale = make_input("Time Scale", "64", "time_scale", tab_id="preview", tooltip="Ratio between XY and time", width=20) + frame_length = make_input("Frame Length", "16", "frame_length", tab_id="preview", suffix="b", tooltip="Number of beats to draw frames for", width=20) apply_button = ui.button("Apply").props("outline") - with ui.expansion("Colors & Sizes", icon="palette").props("dense"): - sp = SettingsPanel() + @ui.refreshable @handle_errors def draw_preview_scene(): diff --git a/src/synth_mapping_helper/gui_tabs/text_gen.py b/src/synth_mapping_helper/gui_tabs/text_gen.py index 51e0ddf..d5e503b 100644 --- a/src/synth_mapping_helper/gui_tabs/text_gen.py +++ b/src/synth_mapping_helper/gui_tabs/text_gen.py @@ -126,15 +126,15 @@ def _open_font_dialog() -> None: letter_rotation = make_input("Angle", "-10", "letter_rotation", suffix="°") with ui.card(): with ui.row(): - with ui.expansion("Settings", icon="settings").props("dense"): + with ui.expansion("Preview Settings", icon="palette").props("dense"): + sp = SettingsPanel() + ui.separator() with ui.row(): + ui.icon("preview", size="3em").tooltip("Change size and scaling of preview") scene_width = make_input("Width", "800", "width", tab_id="preview", suffix="px", tooltip="Width of the preview in px") scene_height = make_input("Height", "600", "height", tab_id="preview", suffix="px", tooltip="Height of the preview in px") - with ui.row(): time_scale = make_input("Time Scale", "64", "time_scale", tab_id="preview", tooltip="Ratio between XY and time") frame_length = make_input("Frame Length", "2", "frame_length", tab_id="preview", suffix="b", tooltip="Number of beats to draw frames for") - with ui.expansion("Colors & Sizes", icon="palette").props("dense"): - sp = SettingsPanel() apply_button = ui.button("Apply").props("outline") def _soft_refresh(copy:bool = True): data = synth_format.ClipboardDataContainer() diff --git a/src/synth_mapping_helper/gui_tabs/wall_art.py b/src/synth_mapping_helper/gui_tabs/wall_art.py index 9ac3ec8..1504116 100644 --- a/src/synth_mapping_helper/gui_tabs/wall_art.py +++ b/src/synth_mapping_helper/gui_tabs/wall_art.py @@ -1,3 +1,4 @@ +from base64 import b64decode, b64encode from dataclasses import dataclass, field from io import BytesIO from time import time @@ -11,7 +12,7 @@ import requests from .map_render import MapScene, SettingsPanel -from .utils import GUITab, SMHInput, ParseInputError, PreventDefaultKeyboard, handle_errors, info, error, safe_clipboard_data +from .utils import GUITab, SMHInput, PrettyError, ParseInputError, PreventDefaultKeyboard, handle_errors, info, error, safe_clipboard_data from ..utils import parse_number, pretty_time_delta, pretty_fraction, pretty_list from .. import synth_format, movement, pattern_generation @@ -31,10 +32,9 @@ def __init__(self, value: bool|None, storage_id: str, tooltip: str|None=None, co if tooltip is not None: self.tooltip(tooltip) -@app.get("/image_proxy") -def image_proxy(url:str) -> Response: - r = requests.get(url) - return Response(content=r.content) +@ui.page("/wall_art_ref_image") +def image_proxy() -> Response: + return Response(content=b64decode(app.storage.user.get("wall_art_ref_image", ""))) def _wall_art_tab() -> None: preview_scene: MapScene|None = None @@ -482,10 +482,52 @@ def _update_symex(_) -> str: return f"{stack_str}Symmetry: {', '.join(enabled) or 'off'}" for inp in (mirror_x, mirror_y, rotsym_direction, rotate_first, turbostack): inp.bind_value_to(sym_exp, "text", forward=_update_symex) + with ui.expansion("Reference", icon="image").props("dense"): + ui.tooltip("Display a reference image to align wall art.") + refimg_url = ui.input("Reference Image URL").props("dense").classes("w-full").bind_value(app.storage.user, "wall_art_ref_image_url").tooltip("Direct URL to image file. Press the download button below to download.") + with ui.row(): + def _clear_image() -> None: + app.storage.user["wall_art_ref_image"] = "" + ui.notify("Reference image cleared. Click APPLY to apply.", type="positive") + ui.button(icon="clear", on_click=_clear_image, color="negative").props("outline").tooltip("Clear image data").classes("w-10") + @handle_errors + def _download_image() -> None: + url = refimg_url.value + if not url: + raise PrettyError(msg="Set an URL above") + else: + try: + r = requests.get(url) + r.raise_for_status() + except requests.RequestException as req_exc: + raise PrettyError(msg="Downloading image failed", exc=req_exc, data=url) from req_exc + data = r.content + app.storage.user["wall_art_ref_image"] = b64encode(data).decode() + ui.notify(f"Downloaded reference image ({len(data)/1024:.1f} KiB). Click APPLY to apply.", type="positive") + ui.button(icon="cloud_download", on_click=_download_image, color="positive").props("outline").tooltip("Download image from URL").classes("w-10") + def _upload_image(e: events.UploadEventArguments) -> None: + upl: ui.upload = e.sender # type:ignore + upl.reset() + data = e.content.read() + app.storage.user["wall_art_ref_image"] = b64encode(data).decode() + ui.notify(f"Uploaded reference image ({len(data)/1024:.1f} KiB). Click APPLY to apply.", type="positive") + refimg_upload = ui.upload(label="Upload File", multiple=True, auto_upload=True, on_upload=_upload_image).props('outline color="positive" accept="image/*"').classes("w-28") + with refimg_upload.add_slot("list"): + pass + with ui.row(): + refimg_width = make_input("Width", "16", "ref_width", suffix="sq", tooltip="Width of the reference image in sq") + refimg_height = make_input("Height", "12", "height", suffix="sq", tooltip="Height of the reference image in sq") + refimg_opacity = make_input("Opacity", "0.5", "ref_opacity", tooltip="Opacity of the image (0-1). 1=completely opaque, 0=completely transparent") + with ui.row(): + refimg_x = make_input("X", "0", "ref_x", suffix="sq", tooltip="Center X of the reference image in sq") + refimg_y = make_input("Y", "0", "ref_y", suffix="sq", tooltip="Center Y of the reference image in sq") + refimg_t = make_input("Time", "1/4", "ref_time", suffix="b", tooltip="Time of the reference image in beats") + refimg_apply_button = ui.button("Apply").props("outline") with ui.expansion("Preview setttings", icon="palette").props("dense"): sp = SettingsPanel() ui.separator() with ui.row(): + ui.icon("highlight_alt", size="3em").tooltip("Appearance of selected walls") move_color = ui.color_input("Move", value="#888888", preview=True).props("dense").classes("w-28").bind_value(app.storage.user, "wall_art_move_color") move_color.button.style("color: black") move_opacity = make_input("Opacity", "0.5", "move_opacity") @@ -494,26 +536,18 @@ def _update_symex(_) -> str: copy_opacity = make_input("Opacity", "0.5", "copy_opacity") ui.separator() with ui.row(): + ui.icon("preview", size="3em").tooltip("Change size and scaling of preview") scene_width = make_input("Width", "800", "width", tab_id="preview", suffix="px", tooltip="Width of the preview in px") scene_height = make_input("Height", "600", "height", tab_id="preview", suffix="px", tooltip="Height of the preview in px") time_scale = make_input("Time Scale", "64", "time_scale", tab_id="preview", tooltip="Ratio between XY and time") frame_length = make_input("Frame Length", "2", "frame_length", tab_id="preview", suffix="b", tooltip="Number of beats to draw frames for") - with ui.row(): - cam_distance = make_input("Camera Distance", "1", "cam_distance", tab_id="preview", suffix="b", tooltip="Default camera distance") - cam_height = make_input("Camera Height", "2", "cam_height", tab_id="preview", suffix="sq", tooltip="Default camera height") ui.separator() - with ui.element(): - ui.tooltip("Display a reference image to align wall art. A low time scale (e.g. 1) is recommended to avoid distortion due to perspective, but may cause display issues.") - refimg_url = ui.input("Reference Image URL").props("dense").classes("w-full").bind_value(app.storage.user, "wall_art_ref_url") - with ui.row(): - refimg_width = make_input("Width", "16", "ref_width", suffix="sq", tooltip="Width of the reference image in sq") - refimg_height = make_input("Height", "12", "height", suffix="sq", tooltip="Height of the reference image in sq") - refimg_opacity = make_input("Opacity", "0.1", "ref_opacity") - with ui.row(): - refimg_x = make_input("X", "0", "ref_x", suffix="sq", tooltip="Center X of the reference image in sq") - refimg_y = make_input("Y", "0", "ref_y", suffix="sq", tooltip="Center Y of the reference image in sq") - refimg_t = make_input("Time", "1/4", "ref_time", suffix="b", tooltip="Time of the reference image in beats") - apply_button = ui.button("Apply").props("outline") + with ui.row(): + ui.icon("camera_indoor", size="3em").tooltip("Change home position of camera") + cam_height = make_input("Cam Height", "2", "cam_height", tab_id="preview", suffix="sq", tooltip="Default camera height") + cam_time = make_input("Cam Time", "1/2", "cam_time", tab_id="preview", suffix="b", tooltip="Default camera time center") + cam_distance = make_input("Cam Distance", "1", "cam_distance", tab_id="preview", suffix="b", tooltip="Default camera distance from center") + preview_apply_button = ui.button("Apply").props("outline") def _find_free_slot(t: float) -> float: while t in walls: t = np.round(t/time_step.parsed_value + 1)*time_step.parsed_value @@ -556,6 +590,7 @@ def _soft_refresh() -> None: preview_settings = sp.parse_settings() if preview_scene is None: draw_preview_scene.refresh() + _reset_camera() if refimg_obj is not None: refimg_obj.delete() refimg_obj = None @@ -565,7 +600,9 @@ def _soft_refresh() -> None: coords = np.array([[[-1/2,0,1/2],[1/2,0,1/2]],[[-1/2,0,-1/2],[1/2,0,-1/2]]]) * [refimg_width.parsed_value,0,refimg_height.parsed_value] pos = (refimg_x.parsed_value, refimg_t.parsed_value*time_scale.parsed_value, refimg_y.parsed_value) opacity = refimg_opacity.parsed_value - refimg_obj = preview_scene.texture(f"/image_proxy?url={refimg_url.value}",coords.tolist()).move(*pos).material(opacity=opacity) + if app.storage.user.get("wall_art_ref_image"): + # add parameter to bypass cache + refimg_obj = preview_scene.texture(f"/wall_art_ref_image?nocache={time()}", coords.tolist()).move(*pos).material(opacity=opacity) wall_data = synth_format.DataContainer(walls=walls) preview_scene.render(wall_data, preview_settings) @@ -574,8 +611,8 @@ def _reset_camera() -> None: if preview_scene is None: return preview_scene.move_camera( - 0, -cam_distance.parsed_value*time_scale.parsed_value, cam_height.parsed_value, - 0,0,0, + 0, (cam_time.parsed_value-cam_distance.parsed_value)*time_scale.parsed_value, cam_height.parsed_value, + 0, cam_time.parsed_value*time_scale.parsed_value, cam_height.parsed_value, duration=0, ) @@ -819,8 +856,9 @@ def _add_wall(e: events.GenericEventArguments, wall_type=i) -> None: ''' ui.html(content) - - apply_button.on("click", draw_preview_scene.refresh) + + refimg_apply_button.on("click", draw_preview_scene.refresh) + preview_apply_button.on("click", draw_preview_scene.refresh) wall_art_tab = GUITab( name="wall_art",