Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c5bc30c
add: towards dimmer brightness mixin
Doralitze Dec 14, 2025
ce652a8
add: finished implementation of filter inst
Doralitze Dec 15, 2025
6d5f57e
add: node implementation of dimmer brightness mixin
Doralitze Dec 16, 2025
b5db3af
fix: documentation in adapter nodes file
Doralitze Dec 16, 2025
8e1bc17
add: config widget
Doralitze Dec 16, 2025
f79752b
fix: missing typo annotation
Doralitze Dec 16, 2025
3907036
chg: global dimmer in fixture inst to vfilter
Doralitze Dec 16, 2025
7650ba3
add: support for fine dimmer channel
Doralitze Dec 16, 2025
756635b
fix: redundant array copy
Doralitze Dec 16, 2025
113e9df
add: towards unit test
Doralitze Dec 18, 2025
61e0e82
add: execute_board_configuration method
Doralitze Dec 18, 2025
b1e80e5
fix: documentation in new unit tests
Doralitze Dec 19, 2025
eebcbb7
fix: bug causing show file loading to not work
Doralitze Dec 20, 2025
fe86d6f
Merge branch 'main' into dimmer-brightness-mixin-vfilter
Doralitze Jan 5, 2026
6c62131
add: improved show file upload error handling to editor
Doralitze Jan 7, 2026
a5f7ebb
add: option to remove assets
Doralitze Jan 10, 2026
63f36dd
add: option to edit icons of macro buttons
Doralitze Jan 10, 2026
66caaf5
add: dialog to ask for exit
Doralitze Jan 10, 2026
07ce689
add: basic template generation for C2CW vFilter
Doralitze Jan 11, 2026
f1c056e
fix: duplicate declaration of closing event in main
Doralitze Jan 12, 2026
964e849
fix: import formatting
Doralitze Jan 12, 2026
25f4180
fix: main window close callback to be private
Doralitze Jan 15, 2026
9172d61
add: ColorToColorWheel support to vfilter_factory
Doralitze Mar 17, 2026
37fd93a
add: method to automatically generate color mapping
Doralitze Mar 25, 2026
e00353f
add: automatic mode support to color wheel vfilter
Doralitze Mar 26, 2026
9401423
add: filter node integration for color2colorwheel adapter
Doralitze Mar 26, 2026
21f93e3
add: towards configuration widget for colorwheel adapter
Doralitze Mar 26, 2026
4a354f3
add: color mapping list implementation
Doralitze Mar 27, 2026
20e0d8c
add: towards color wheel parsing from OFL
Doralitze Apr 2, 2026
413654f
add: fixture selection in config widget
Doralitze Apr 2, 2026
2aa0ca4
add: combobox options in config widget
Doralitze Apr 2, 2026
91cc647
add: color name parsing
Doralitze Apr 3, 2026
f91c989
fix: fixture color wheel decoding
Doralitze Apr 4, 2026
0b582f1
fix: disappearing FSI after removing dimmer channels
Doralitze Apr 5, 2026
c28c383
fix: first half of ruff issues
Doralitze Apr 5, 2026
7c86141
fix: remaining ruff issues
Doralitze Apr 5, 2026
1f5bff2
fix: new ruff rule
Doralitze Apr 7, 2026
f686f81
chg: more elaborated requests by ruff
Doralitze Apr 7, 2026
910d64d
add: color from temperature
Doralitze Apr 9, 2026
85deb1e
fix: first documentation instances
Doralitze Apr 9, 2026
56125ea
fix: docs in filter node classes
Doralitze Apr 9, 2026
89acc9f
fix: docs in color selection UI widget
Doralitze Apr 9, 2026
c5fe98b
fix: requested autotracker docs
Doralitze Apr 9, 2026
bfffe55
fix: doc string in node_editor_widget.py
Doralitze Apr 9, 2026
5afc4c6
add: dmx default value support to scene model
Doralitze Apr 15, 2026
e474638
mrg: branch '169-color-to-color-wheel-adapter-v-filter' into scene-de…
Doralitze Apr 15, 2026
c17dd43
mrg: branch 'media-asset-config-ui' into scene-default-dmx-values
Doralitze Apr 15, 2026
bf70dc5
mrg: branch 'dimmer-brightness-mixin-vfilter' into scene-default-dmx-…
Doralitze Apr 15, 2026
4270515
fix: ruff issues
Doralitze Apr 15, 2026
91ff1d5
add: skeleton
Doralitze Apr 15, 2026
81e7154
add: tab loading and closing mechanics
Doralitze Apr 15, 2026
1722447
add: crude editing UI
Doralitze Apr 15, 2026
fadad97
fix: inverted update bug
Doralitze Apr 15, 2026
4ce9b85
fix: docs in showmanager.py
Doralitze Apr 15, 2026
650b463
fix: typo
Doralitze Apr 15, 2026
4521faf
add: entry removal option
Doralitze Apr 16, 2026
d63816c
add: default mapping from console
Doralitze Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/controller/cli/cli_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def __init__(self, show: BoardConfiguration, network_manager: NetworkManager, ex
help="subcommands help", dest="subparser_name"
)
for c in self._commands:
c.configure_parser(subparsers.add_parser(c.name, help=c.help, exit_on_error=False))
c.configure_parser(subparsers.add_parser(c.name, help=c.help_text, exit_on_error=False))
if exit_available:
subparsers.add_parser("exit", exit_on_error=False, help="Close this remote connection")
self._return_text = ""
Expand Down
6 changes: 3 additions & 3 deletions src/controller/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ def name(self) -> str:
return "Unnamed Command"

@property
def help(self) -> str:
def help_text(self) -> str:
"""Help text."""
return self._help_text

@help.setter
def help(self, new_help: str) -> None:
@help_text.setter
def help_text(self, new_help: str) -> None:
self._help_text = str(new_help)
13 changes: 12 additions & 1 deletion src/controller/file/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from controller.file.deserialization.migrations import replace_old_filter_configurations
from controller.file.deserialization.post_load_operations import link_patched_fixtures
from controller.utils.process_notifications import get_process_notifier
from model import BoardConfiguration, ColorHSI, Filter, Scene, UIPage, Universe
from model import BoardConfiguration, Filter, Scene, UIPage, Universe
from model.color_hsi import ColorHSI
from model.control_desk import BankSet, ColorDeskColumn, FaderBank, RawDeskColumn
from model.events import EventSender, mark_sender_persistent
from model.filter import VirtualFilter
Expand Down Expand Up @@ -264,6 +265,14 @@ def _parse_filter_page(element: ET.Element, parent_scene: Scene, instantiated_pa
return True


def _parse_dmx_default_value(scene: Scene, child: ET.Element) -> None:
scene.insert_dmx_default_value(
int(child.attrib["universe"]),
int(child.attrib["channel"]),
int(child.attrib["value"])
)


def _parse_scene(
scene_element: ET.Element, board_configuration: BoardConfiguration, loaded_banksets: dict[str, BankSet]
) -> None:
Expand Down Expand Up @@ -300,6 +309,8 @@ def _parse_scene(
filter_pages.append(child)
case "uipage":
ui_page_elements.append(child)
case "dmxdefaultvalue":
_parse_dmx_default_value(scene, child)
case _:
logger.warning("Scene %s contains unknown element: %s", human_readable_name, child.tag)

Expand Down
7 changes: 7 additions & 0 deletions src/controller/file/serializing/scene_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def generate_scene_xml_description(

for ui_page in scene.ui_pages:
_add_ui_page_to_element(scene_element, ui_page)
scene.sort_dmx_default_values()
for default_value in scene.dmx_default_values:
ET.SubElement(scene_element, "dmxdefaultvalue", attrib={
"universe": default_value.universe_id,
"channel": str(default_value.channel),
"value": str(default_value.value),
})


def _create_scene_element(scene: Scene, parent: ET.Element) -> ET.Element:
Expand Down
2 changes: 1 addition & 1 deletion src/controller/joystick/joystick_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ def reformat(key: Key) -> None:

def __new__(cls) -> Self:
"""Connect a joystick and setup the key bindings."""
mngr = pyjoystick.ThreadEventManager(event_loop=run_event_loop, handle_key_event=lambda key: cls.reformat(key))
mngr = pyjoystick.ThreadEventManager(event_loop=run_event_loop, handle_key_event=cls.reformat)
mngr.start()
4 changes: 2 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

if __name__ == "__main__":
import os
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QSplashScreen

QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL)
app = QApplication([])
app = QApplication(sys.argv)
from PySide6.QtGui import QPixmap

from utility import resource_path
Expand Down Expand Up @@ -44,7 +45,6 @@
import logging.config
import logging.handlers
import pathlib
import sys

from PySide6.QtCore import QEventLoop

Expand Down
3 changes: 1 addition & 2 deletions src/model/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Public classes and methods from the model package"""
"""Public classes and methods from the model package."""

from .board_configuration import BoardConfiguration
from .broadcaster import Broadcaster
from .color_hsi import ColorHSI
from .device import Device
from .filter import DataType, Filter
from .scene import Scene
Expand Down
37 changes: 33 additions & 4 deletions src/model/board_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections.abc import Callable, Sequence
from logging import getLogger
from uuid import UUID

import numpy as np
from PySide6 import QtCore, QtGui
Expand Down Expand Up @@ -30,7 +31,7 @@ def __init__(self, show_name: str = "", default_active_scene: int = 0, notes: st
self._scenes_index: dict[int, int] = {}
self._devices: list[Device] = []
self._universes: dict[int, Universe] = {}
self._fixtures: list[UsedFixture] = []
self._fixtures: dict[str, UsedFixture] = {}
self._ui_hints: dict[str, str] = {}
self._macros: list[Macro] = []

Expand Down Expand Up @@ -100,7 +101,7 @@ def _add_universe(self, universe: Universe) -> None:
self._universes.update({universe.id: universe})

def _add_fixture(self, used_fixture: UsedFixture) -> None:
self._fixtures.append(used_fixture)
self._fixtures[str(used_fixture.uuid)] = used_fixture

def _delete_universe(self, universe: Universe) -> None:
"""Remove the passed universe from the list of universes.
Expand All @@ -110,7 +111,10 @@ def _delete_universe(self, universe: Universe) -> None:

"""
try:
del self._universes[universe.id]
uid = universe.id if isinstance(universe, Universe) else universe if isinstance(universe, int) else None
if uid is None:
raise ValueError("Expected a universe.")
del self._universes[uid]
except ValueError:
logger.exception("Unable to remove universe %s", universe.name)

Expand Down Expand Up @@ -146,7 +150,7 @@ def universe(self, universe_id: int) -> Universe | None:
@property
def fixtures(self) -> Sequence[UsedFixture]:
"""Fixtures associated with this Show."""
return self._fixtures
return self._fixtures.values()

@property
def show_name(self) -> str:
Expand Down Expand Up @@ -312,3 +316,28 @@ def get_occupied_channels(self, universe_id: int) -> np.typing.NDArray[int]:
]

return np.concatenate(ranges) if ranges else np.array([], dtype=int)

def get_fixture(self, fixture_id: str | UUID) -> UsedFixture | None:
"""Get the fixture specified by its id."""
if fixture_id == "" or fixture_id is None:
return None
if isinstance(fixture_id, UUID):
fixture_id = str(fixture_id)
return self._fixtures.get(fixture_id)

def get_fixture_by_address(self, fixture_univ: int, fixture_chan: int) -> UsedFixture | None:
"""Search for a fixture matching the provided address.

Args:
fixture_univ: The universe of the fixture.
fixture_chan: A channel of the fixture.

Returns:
The fixture or None if no fixture was found.

"""
for fixture in self._fixtures.values():
if fixture.universe_id == fixture_univ and \
fixture.start_index <= fixture_chan < fixture.start_index + fixture.channel_length:
return fixture
return None
1 change: 1 addition & 0 deletions src/model/broadcaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Broadcaster(QtCore.QObject, metaclass=QObjectSingletonMeta):
scene_open_in_editor_requested: QtCore.Signal = QtCore.Signal(object) # FilterPage
bankset_open_in_editor_requested: QtCore.Signal = QtCore.Signal(dict)
uipage_opened_in_editor_requested: QtCore.Signal = QtCore.Signal(dict)
default_dmx_value_editor_opening_requested: QtCore.Signal = QtCore.Signal(object)
delete_scene: QtCore.Signal = QtCore.Signal(object)
delete_universe: QtCore.Signal = QtCore.Signal(object)
device_created: QtCore.Signal = QtCore.Signal(object) # device
Expand Down
57 changes: 57 additions & 0 deletions src/model/color_hsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import colorsys
from typing import TYPE_CHECKING

import numpy as np
from PySide6.QtGui import QColor

if TYPE_CHECKING:
Expand Down Expand Up @@ -55,6 +56,62 @@ def from_filter_str(cls, filter_format: str) -> ColorHSI:
intensity = 1.0
return ColorHSI(hue, saturation, intensity)

@classmethod
def from_rgb(cls, red: int, green: int, blue: int) -> ColorHSI:
"""Initialize an HSI color from the given RGB color.

Args:
red: Red component of the color. It must be in the range [0, 255]
green: Green component of the color. It must be in the range [0, 255]
blue: Blue component of the color. It must be in the range [0, 255]

Returns:
The HSI color object.

"""
hue, luminescence, saturation = colorsys.rgb_to_hls(red / 255, green / 255, blue / 255)
return ColorHSI(hue, luminescence, saturation)

@classmethod
def from_color_temperature(cls, temperature: float | str) -> ColorHSI:
"""Initialize an HSI color from the given color temperature.

If a string is provided it will be converted automatically.

Args:
temperature: Color temperature in degrees Kelvin.

Returns:
A ColorHSI object representing the given color temperature.

"""

def cutoff(value: float, min_val: int, max_val: int) -> int:
return max(min(int(value), max_val), min_val)

if isinstance(temperature, str):
temperature = float(temperature.lower().replace(" ", "").replace("k", ""))
temperature = temperature / 100
if temperature <= 66:
red = 255
green = temperature
green = 99.4708025861 * np.log(green) - 161.1195681661
if temperature <= 19:
blue = 0
else:
blue = temperature - 10
blue = 138.5177312231 * np.log(blue) - 305.0447927307
else:
red = temperature - 60
red = 329.698727446 * (red ** -0.1332047592)
green = temperature - 60
green = 288.1221695283 * (green ** -0.0755148492)
blue = 255
red = cutoff(red, 0, 255)
green = cutoff(green, 0, 255)
blue = cutoff(blue, 0, 255)
return ColorHSI.from_rgb(red, green, blue)

@property
def hue(self) -> confloat(ge=0, le=360):
"""Color itself in the form of an angle between [0,360] degrees."""
Expand Down
2 changes: 1 addition & 1 deletion src/model/control_desk.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, uid: str | None = None) -> None:
uid: Unique identifier for the column.

"""
self.id = uid if uid else _generate_unique_id()
self.id = uid or _generate_unique_id()
self.bank_set: BankSet | None = None
self._bottom_display_line_inverted = False
self._top_display_line_inverted = False
Expand Down
10 changes: 6 additions & 4 deletions src/model/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class FilterTypeEnumeration(IntFlag):
Negative values indicate virtual filters.
"""

VFILTER_COLOR_TO_COLORWHEEL = -14
VFILTER_DIMMER_BRIGHTNESS_MIXIN = -13
VFILTER_SEQUENCER = -12
VFILTER_COLOR_MIXER = -11
VFILTER_IMPORT = -10
Expand Down Expand Up @@ -303,16 +305,16 @@ def copy(self, new_scene: Scene = None, new_id: str | None = None) -> Filter:

if self.is_virtual_filter:
f = construct_virtual_filter_instance(
new_scene if new_scene else self.scene,
new_scene or self.scene,
self._filter_type,
new_id if new_id else self._filter_id,
new_id or self._filter_id,
pos=self._pos,
)
f.filter_configurations.update(self.filter_configurations.copy())
else:
f = Filter(
new_scene if new_scene else self.scene,
new_id if new_id else self._filter_id,
new_scene or self.scene,
new_id or self._filter_id,
self._filter_type,
self._pos,
self.filter_configurations.copy(),
Expand Down
3 changes: 2 additions & 1 deletion src/model/filter_data/cues/cue.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from logging import getLogger
from typing import TYPE_CHECKING, Never, Union, override

from model import ColorHSI, DataType
from model import DataType
from model.color_hsi import ColorHSI
from model.filter_data.transfer_function import TransferFunction
from model.filter_data.utility import format_seconds

Expand Down
3 changes: 2 additions & 1 deletion src/model/filter_data/sequencer/sequencer_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from enum import Enum
from logging import getLogger

from model import ColorHSI, DataType
from model import DataType
from model.color_hsi import ColorHSI
from model.filter_data.sequencer._utils import _rf

logger = getLogger(__name__)
Expand Down
3 changes: 2 additions & 1 deletion src/model/filter_data/sequencer/transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from types import MappingProxyType
from typing import TYPE_CHECKING

from model import ColorHSI, DataType
from model import DataType
from model.color_hsi import ColorHSI
from model.filter_data.cues.cue import Cue, KeyFrame, State, StateColor, StateDouble, StateEightBit, StateSixteenBit
from model.filter_data.sequencer._utils import _rf
from model.filter_data.transfer_function import TransferFunction
Expand Down
6 changes: 5 additions & 1 deletion src/model/media_assets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING
from uuid import uuid4

from model.media_assets.registry import register
from model.media_assets.registry import register, unregister

if TYPE_CHECKING:
from PySide6.QtGui import QPixmap
Expand Down Expand Up @@ -102,3 +102,7 @@ def is_local_resource(self) -> bool:
Non-local resources need to be provided and copied together with the show file.
"""
return False

def unregister(self) -> None:
"""Unregister this asset."""
unregister(self)
19 changes: 19 additions & 0 deletions src/model/media_assets/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ def register(asset: MediaAsset, uuid: str) -> bool:
d[uuid] = asset
return True

def unregister(asset: MediaAsset) -> bool:
"""Method unregisters a media asset.

Args:
asset: the media asset to unregister

Returns:
True if the asset was successfully unregistered, False otherwise.

"""
reg = _asset_library.get(asset.get_type())
if reg is None:
return False
try:
reg.pop(asset.id)
return True
except KeyError:
return False

def get_asset_by_uuid(uuid: str) -> MediaAsset | None:
"""Get a media asset by its UUID.

Expand Down
Loading
Loading