Initial settings rewrite into modules in preparation for resetting to defaults and for better settings handling

This commit is contained in:
lorow 2023-10-03 22:11:52 +02:00
parent 62fba485ba
commit c93e279157
9 changed files with 352 additions and 259 deletions

View File

@ -1,6 +1,6 @@
import PySimpleGUI as sg import PySimpleGUI as sg
from config import EyeTrackSettingsConfig from config import EyeTrackConfig
from osc import EyeId from osc import EyeId
from queue import Queue from queue import Queue
from threading import Event from threading import Event
@ -8,7 +8,7 @@ from threading import Event
class AlgoSettingsWidget: class AlgoSettingsWidget:
def __init__( def __init__(
self, widget_id: EyeId, main_config: EyeTrackSettingsConfig, osc_queue: Queue self, widget_id: EyeId, main_config: EyeTrackConfig, osc_queue: Queue
): ):
self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-" self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-"
@ -522,5 +522,4 @@ class AlgoSettingsWidget:
if changed: if changed:
self.main_config.save() self.main_config.save()
# print(self.main_config)
self.osc_queue.put(EyeId.ALGOSETTINGS) self.osc_queue.put(EyeId.ALGOSETTINGS)

View File

@ -88,8 +88,8 @@ class EyeTrackConfig(BaseModel):
right_eye: EyeTrackCameraConfig = EyeTrackCameraConfig() right_eye: EyeTrackCameraConfig = EyeTrackCameraConfig()
left_eye: EyeTrackCameraConfig = EyeTrackCameraConfig() left_eye: EyeTrackCameraConfig = EyeTrackCameraConfig()
settings: EyeTrackSettingsConfig = EyeTrackSettingsConfig() settings: EyeTrackSettingsConfig = EyeTrackSettingsConfig()
# algo_settings: EyeTrackSettingsConfig = EyeTrackSettingsConfig()
eye_display_id: EyeId = EyeId.RIGHT eye_display_id: EyeId = EyeId.RIGHT
__listeners = []
@staticmethod @staticmethod
def load(): def load():
@ -114,6 +114,13 @@ class EyeTrackConfig(BaseModel):
load_config = EyeTrackConfig() load_config = EyeTrackConfig()
return load_config return load_config
def update(self, data, save=False):
for field, value in data.items():
setattr(self.settings, field, value)
self.__notify_listeners()
if save:
self.save()
def save(self): def save(self):
# make sure this is only called if there is a change # make sure this is only called if there is a change
if os.path.exists(CONFIG_FILE_NAME): if os.path.exists(CONFIG_FILE_NAME):
@ -129,3 +136,11 @@ class EyeTrackConfig(BaseModel):
with open(CONFIG_FILE_NAME, "w") as settings_file: with open(CONFIG_FILE_NAME, "w") as settings_file:
json.dump(obj=self.dict(), fp=settings_file) json.dump(obj=self.dict(), fp=settings_file)
print(f"\033[92m[INFO] Config Saved Successfully\033[0m") print(f"\033[92m[INFO] Config Saved Successfully\033[0m")
def register_listener_callback(self, callback):
print(f"[DEBUG] Registering listener {callback}")
self.__listeners.append(callback)
def __notify_listeners(self):
for listener in self.__listeners:
listener()

View File

@ -90,7 +90,7 @@ def main():
] ]
settings = [ settings = [
SettingsWidget(EyeId.SETTINGS, config, osc_queue), SettingsWidget(EyeId.SETTINGS, config),
AlgoSettingsWidget(EyeId.ALGOSETTINGS, config, osc_queue), AlgoSettingsWidget(EyeId.ALGOSETTINGS, config, osc_queue),
] ]
@ -282,6 +282,5 @@ def main():
setting.render(window, event, values) setting.render(window, event, values)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,201 +1,54 @@
import PySimpleGUI as sg import PySimpleGUI as sg
from config import EyeTrackSettingsConfig from config import EyeTrackConfig, EyeTrackSettingsConfig
from osc import EyeId from osc import EyeId
from queue import Queue
from threading import Event from threading import Event
class SettingsWidget: from settings.modules.GeneralSettingsModule import GeneralSettingsModule
def __init__(self, widget_id: EyeId, main_config: EyeTrackSettingsConfig, osc_queue: Queue): from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule
from settings.modules.OSCSettingsModule import OSCSettingsModule
self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-"
self.gui_flip_x_axis_right = f"-FLIPXAXISRIGHT{widget_id}-" class SettingsWidget:
self.gui_flip_y_axis = f"-FLIPYAXIS{widget_id}-" def __init__(self, widget_id: EyeId, main_config: EyeTrackConfig):
self.gui_general_settings_layout = f"-GENERALSETTINGSLAYOUT{widget_id}-" self.is_saving = False
self.gui_osc_address = f"-OSCADDRESS{widget_id}-"
self.gui_osc_port = f"-OSCPORT{widget_id}-"
self.gui_osc_receiver_port = f"OSCRECEIVERPORT{widget_id}-"
self.gui_osc_recenter_address = f"OSCRECENTERADDRESS{widget_id}-"
self.gui_osc_recalibrate_address = f"OSCRECALIBRATEADDRESS{widget_id}-"
self.gui_BLOB = f"-BLOBFALLBACK{widget_id}-"
self.gui_HSF = f"-HSF{widget_id}-"
self.gui_DADDY = f"-DADDY{widget_id}-"
self.gui_DADDYP = f"-DADDYP{widget_id}-"
self.gui_RANSAC3D = f"-RANSAC3D{widget_id}-"
self.gui_BLINK = f"-BLINK{widget_id}-"
self.gui_IBO = f"-IBO{widget_id}-"
self.gui_HSRAC = f"-HSRAC{widget_id}-"
self.gui_HSF_radius = f"-HSFRADIUS{widget_id}-"
self.gui_blob_maxsize = f"-BLOBMAXSIZE{widget_id}-"
self.gui_blob_minsize = f"-BLOBMINSIZE{widget_id}-"
self.gui_speed_coefficient = f"-SPEEDCOEFFICIENT{widget_id}-"
self.gui_min_cutoff = f"-MINCUTOFF{widget_id}-"
self.gui_eye_falloff = f"-EYEFALLOFF{widget_id}-"
self.gui_skip_autoradius = f"-SKIPAUTORADIUS{widget_id}-"
self.gui_HSRACP = f"-HSRACP{widget_id}-"
self.gui_RANSAC3DP = f"-RANSAC3DP{widget_id}-"
self.gui_HSFP = f"-HSFP{widget_id}-"
self.gui_BLOBP = f"-BLOBP{widget_id}-"
self.gui_thresh_add = f"-THRESHADD{widget_id}-"
self.gui_ROSC = f"-ROSC{widget_id}-"
self.gui_vrc_native = f"-VRCNATIVE{widget_id}-"
self.gui_circular_crop_left = f"-CIRCLECROPLEFT{widget_id}-"
self.gui_circular_crop_right = f"-CIRCLECROPRIGHT{widget_id}-"
self.gui_update_check = f"-UPDATECHECK{widget_id}-"
self.gui_threshold_slider = f"-BLOBTHRESHOLD{widget_id}-"
self.main_config = main_config self.main_config = main_config
self.config = main_config.settings self.config = main_config.settings
self.osc_queue = osc_queue
settings_modules = [
GeneralSettingsModule,
OneEuroSettingsModule,
OSCSettingsModule,
]
self.initialized_modules = self._initialize_modules(
settings_modules=settings_modules, widget_id=widget_id
)
self.gui_general_settings_layout = f"-GENERALSETTINGSLAYOUT{widget_id}-"
# Define the window's contents # Define the window's contents
self.general_settings_layout = [ self.general_settings_layout = []
[
sg.Checkbox(
"Flip Left Eye X Axis",
default=self.config.gui_flip_x_axis_left,
key=self.gui_flip_x_axis_left,
background_color='#424042',
tooltip = "Flips the left eye's X axis.",
),
sg.Checkbox(
"Flip Right Eye X Axis",
default=self.config.gui_flip_x_axis_right,
key=self.gui_flip_x_axis_right,
background_color='#424042',
tooltip = "Flips the right eye's X axis.",
),
# ],
#[
sg.Checkbox(
"Flip Y Axis",
default=self.config.gui_flip_y_axis,
key=self.gui_flip_y_axis,
background_color='#424042',
tooltip = "Flips the eye's Y axis.",
),
],
[sg.Checkbox(
"VRC Native Eyetracking",
default=self.config.gui_vrc_native,
key=self.gui_vrc_native,
background_color='#424042',
tooltip = "Toggle VRCFT output or VRC native",
),
sg.Checkbox(
"Dual Eye Falloff",
default=self.config.gui_eye_falloff,
key=self.gui_eye_falloff,
background_color='#424042',
tooltip = "If one eye stops tracking, we send tracking data from your other eye.",
),
],
[sg.Checkbox(
"Check For Updates",
default=self.config.gui_update_check,
key=self.gui_update_check,
background_color='#424042',
tooltip = "Toggle update check on launch.",
),
],
[
sg.Text("One Euro Filter Paramaters:", background_color='#242224'),
],
[
sg.Text("Min Frequency Cutoff", background_color='#424042'),
sg.InputText(
self.config.gui_min_cutoff,
key=self.gui_min_cutoff,
size=(0,10),
),
#],
#[
sg.Text("Speed Coefficient", background_color='#424042'),
sg.InputText(
self.config.gui_speed_coefficient,
key=self.gui_speed_coefficient,
size=(0,10),
),
],
[
sg.Text("OSC Settings:", background_color='#242224'),
],
[
sg.Text("Address:", background_color='#424042'),
sg.InputText(
self.config.gui_osc_address,
key=self.gui_osc_address,
size=(0,20),
tooltip = "IP address we send OSC data to.",
),
# ],
# [
sg.Text("Port:", background_color='#424042'),
sg.InputText(
self.config.gui_osc_port,
key=self.gui_osc_port,
size=(0,10),
tooltip = "OSC port we send data to.",
),
],
[
sg.Text("Receive functions", background_color='#424042'),
sg.Checkbox(
"",
default=self.config.gui_ROSC,
key=self.gui_ROSC,
background_color='#424042',
size=(0,10),
tooltip = "Toggle OSC receive functions.",
),
],
[
sg.Text("Receiver Port:", background_color='#424042'),
sg.InputText(
self.config.gui_osc_receiver_port,
key=self.gui_osc_receiver_port,
size=(0,10),
tooltip = "Port we receive OSC data from (used to recalibrate or recenter app from within VRChat.",
),
#],
# [
sg.Text("Recenter Address:", background_color='#424042'),
sg.InputText(
self.config.gui_osc_recenter_address,
key=self.gui_osc_recenter_address,
size=(0,10),
tooltip = "OSC Address used for recentering your eye.",
),
],
[
sg.Text("Recalibrate Address:", background_color='#424042'),
sg.InputText(
self.config.gui_osc_recalibrate_address,
key=self.gui_osc_recalibrate_address,
size=(0,10),
tooltip = "OSC address we use for recalibrating your eye",
),
]
]
for module in self.initialized_modules:
self.general_settings_layout.extend(module.get_layout())
self.widget_layout = [ self.widget_layout = [
[ [
sg.Text("General Settings:", background_color='#242224'), sg.Text("General Settings:", background_color="#242224"),
], ],
[ [
sg.Column(self.general_settings_layout, key=self.gui_general_settings_layout, background_color='#424042' ), sg.Column(
self.general_settings_layout,
key=self.gui_general_settings_layout,
background_color="#424042",
),
], ],
] ]
self.cancellation_event = Event() # Set the event until start is called, otherwise we can block if shutdown is called. self.cancellation_event = (
Event()
) # Set the event until start is called, otherwise we can block if shutdown is called.
self.cancellation_event.set() self.cancellation_event.set()
self.image_queue = Queue()
def started(self): def started(self):
return not self.cancellation_event.is_set() return not self.cancellation_event.is_set()
@ -212,81 +65,38 @@ class SettingsWidget:
return return
self.cancellation_event.set() self.cancellation_event.set()
def _update_and_save_config(self, validated_data: dict):
self.main_config.update(validated_data, save=True)
self.is_saving = False
def _handle_errors(self, errors):
print(errors)
def render(self, window, event, values): def render(self, window, event, values):
# If anything has changed in our configuration settings, change/update those. validated_data, errors = {}, []
changed = False # we might want to think about event driven architecture here eventually, validate only
# if anything changes instead of checking for changes
for module in self.initialized_modules:
module_validated_data = module.validate(values)
if module_validated_data.changes:
validated_data.update(module_validated_data.changes)
if module_validated_data.errors:
errors.append(module_validated_data.errors)
if self.config.gui_osc_port != int(values[self.gui_osc_port]): if not errors and validated_data and not self.is_saving:
print(self.config.gui_osc_port, values[self.gui_osc_port]) self.is_saving = True
try: self._update_and_save_config(validated_data)
int(values[self.gui_osc_port])
if len(values[self.gui_osc_port]) <= 5:
self.config.gui_osc_port = int(values[self.gui_osc_port])
changed = True
else:
print("\033[91m[ERROR] OSC port value must be an integer 0-65535\033[0m")
except:
print("\033[91m[ERROR] OSC port value must be an integer 0-65535\033[0m")
if self.config.gui_osc_receiver_port != int(values[self.gui_osc_receiver_port]): if errors:
try: self._handle_errors(errors)
int(values[self.gui_osc_receiver_port])
if len(values[self.gui_osc_receiver_port]) <= 5:
self.config.gui_osc_receiver_port = int(values[self.gui_osc_receiver_port])
changed = True
else:
print("\033[91m[ERROR] OSC receive port value must be an integer 0-65535\033[0m")
except:
print("\033[91m[ERROR] OSC receive port value must be an integer 0-65535\033[0m")
if self.config.gui_osc_address != values[self.gui_osc_address]: def _initialize_modules(self, settings_modules, widget_id):
self.config.gui_osc_address = values[self.gui_osc_address] return [
changed = True module(
config=self.config,
if self.config.gui_osc_recenter_address != values[self.gui_osc_recenter_address]: settings=self.main_config,
self.config.gui_osc_recenter_address = values[self.gui_osc_recenter_address] settings_base_class=EyeTrackSettingsConfig,
changed = True widget_id=widget_id,
)
if self.config.gui_osc_recalibrate_address != values[self.gui_osc_recalibrate_address]: for module in settings_modules
self.config.gui_osc_recalibrate_address = values[self.gui_osc_recalibrate_address] ]
changed = True
if self.config.gui_min_cutoff != values[self.gui_min_cutoff]:
self.config.gui_min_cutoff = values[self.gui_min_cutoff]
changed = True
if self.config.gui_speed_coefficient != values[self.gui_speed_coefficient]:
self.config.gui_speed_coefficient = values[self.gui_speed_coefficient]
changed = True
if self.config.gui_flip_x_axis_right != values[self.gui_flip_x_axis_right]:
self.config.gui_flip_x_axis_right = values[self.gui_flip_x_axis_right]
changed = True
if self.config.gui_flip_x_axis_left != values[self.gui_flip_x_axis_left]:
self.config.gui_flip_x_axis_left = values[self.gui_flip_x_axis_left]
changed = True
if self.config.gui_vrc_native != values[self.gui_vrc_native]:
self.config.gui_vrc_native = values[self.gui_vrc_native]
changed = True
if self.config.gui_update_check != values[self.gui_update_check]:
self.config.gui_update_check = values[self.gui_update_check]
changed = True
if self.config.gui_flip_y_axis != values[self.gui_flip_y_axis]:
self.config.gui_flip_y_axis = values[self.gui_flip_y_axis]
changed = True
if self.config.gui_eye_falloff != values[self.gui_eye_falloff]:
self.config.gui_eye_falloff = values[self.gui_eye_falloff]
changed = True
if self.config.gui_ROSC != values[self.gui_ROSC]:
self.config.gui_ROSC = values[self.gui_ROSC]
changed = True
if changed:
self.main_config.save()
self.osc_queue.put((EyeId.SETTINGS))

View File

@ -0,0 +1 @@
BACKGROUND_COLOR = "#424042"

View File

@ -0,0 +1,73 @@
from typing import Iterable, Optional, NamedTuple, Any
import pydantic
from pydantic import BaseModel
from config import EyeTrackSettingsConfig
class MissingValidationModelException(Exception):
pass
class MissingValidationConfigException(Exception):
pass
class BaseValidationModel(BaseModel):
pass
class ValidationResult(NamedTuple):
changes: Optional[dict[str, str]]
errors: list[Any] # ErrorDict but we cannot import it, it's not exposed
class BaseSettingsModule:
def __init__(self, config: EyeTrackSettingsConfig, settings_base_class, widget_id, **kwargs):
self.validation_model: BaseValidationModel = BaseValidationModel # noqa
self.config = config
self.settings_base_class = settings_base_class
self.widget_id = widget_id
def get_validation_model(self):
"""Return validation model, can be overridden for custom behaviour"""
return self.validation_model
def initialize_validation_model(self, values):
validation_model = self.get_validation_model()
if not validation_model:
raise MissingValidationModelException()
field_mapping = {}
for field in self.validation_model.schema().get("properties"):
field_mapping[field] = values[getattr(self, field)]
return validation_model(**field_mapping)
def validate(self, values, raise_exception=False) -> Optional[ValidationResult]:
"""Return """
if not self.config:
raise MissingValidationConfigException()
try:
changes = {}
validated_model = self.initialize_validation_model(values)
# this is empty
for field, value in validated_model.dict().items():
if getattr(self.config, field) != value:
changes[field] = value
return ValidationResult(changes, [])
except pydantic.ValidationError as e:
if not raise_exception:
return ValidationResult(None, e.errors())
raise
def get_layout(self) -> Iterable:
raise NotImplementedError
def get_defaults(self) -> dict:
defaults = {}
for field in self.validation_model.schema().keys():
defaults[field] = getattr(self.settings_base_class, field)
return defaults

View File

@ -0,0 +1,76 @@
from config import EyeTrackSettingsConfig
from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel
import PySimpleGUI as sg
class GeneralSettingsValidationModel(BaseValidationModel):
gui_flip_x_axis_left: bool
gui_flip_x_axis_right: bool
gui_flip_y_axis: bool
gui_vrc_native: bool
gui_eye_falloff: bool
gui_update_check: bool
class GeneralSettingsModule(BaseSettingsModule):
def __init__(self, config, widget_id, settings_base_class, **kwargs):
super().__init__(config, widget_id, settings_base_class, **kwargs)
self.validation_model = GeneralSettingsValidationModel
self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-"
self.gui_flip_x_axis_right = f"-FLIPXAXISRIGHT{widget_id}-"
self.gui_flip_y_axis = f"-FLIPYAXIS{widget_id}-"
self.gui_eye_falloff = f"-EYEFALLOFF{widget_id}-"
self.gui_vrc_native = f"-VRCNATIVE{widget_id}-"
self.gui_update_check = f"-UPDATECHECK{widget_id}-"
def get_layout(self):
return [
[
sg.Checkbox(
"Flip Left Eye X Axis",
default=self.config.gui_flip_x_axis_left,
key=self.gui_flip_x_axis_left,
background_color="#424042",
tooltip="Flips the left eye's X axis.",
),
sg.Checkbox(
"Flip Right Eye X Axis",
default=self.config.gui_flip_x_axis_right,
key=self.gui_flip_x_axis_right,
background_color="#424042",
tooltip="Flips the right eye's X axis.",
),
sg.Checkbox(
"Flip Y Axis",
default=self.config.gui_flip_y_axis,
key=self.gui_flip_y_axis,
background_color="#424042",
tooltip="Flips the eye's Y axis.",
),
],
[
sg.Checkbox(
"VRC Native Eyetracking",
default=self.config.gui_vrc_native,
key=self.gui_vrc_native,
background_color="#424042",
tooltip="Toggle VRCFT output or VRC native",
),
sg.Checkbox(
"Dual Eye Falloff",
default=self.config.gui_eye_falloff,
key=self.gui_eye_falloff,
background_color="#424042",
tooltip="If one eye stops tracking, we send tracking data from your other eye.",
),
],
[
sg.Checkbox(
"Check For Updates",
default=self.config.gui_update_check,
key=self.gui_update_check,
background_color="#424042",
tooltip="Toggle update check on launch.",
),
],
]

View File

@ -0,0 +1,83 @@
from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel
from settings.constants import BACKGROUND_COLOR
import PySimpleGUI as sg
class OSCValidationModel(BaseValidationModel):
gui_osc_port: int
gui_osc_address: str
gui_ROSC: bool
gui_osc_receiver_port: int
gui_osc_recenter_address: str
gui_osc_recalibrate_address: str
class OSCSettingsModule(BaseSettingsModule):
def __init__(self, config, widget_id, settings_base_class, **kwargs):
super().__init__(config, widget_id, settings_base_class, **kwargs)
self.validation_model = OSCValidationModel
self.gui_osc_address = f"-OSCADDRESS{widget_id}-"
self.gui_osc_port = f"-OSCPORT{widget_id}-"
self.gui_ROSC = f"-ROSC{widget_id}-"
self.gui_osc_receiver_port = f"OSCRECEIVERPORT{widget_id}-"
self.gui_osc_recenter_address = f"OSCRECENTERADDRESS{widget_id}-"
self.gui_osc_recalibrate_address = f"OSCRECALIBRATEADDRESS{widget_id}-"
def get_layout(self):
return [
[
sg.Text("OSC Settings:", background_color="#242224"),
],
[
sg.Text("Address:", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_osc_address,
key=self.gui_osc_address,
size=(0, 20),
tooltip="IP address we send OSC data to.",
),
sg.Text("Port:", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_osc_port,
key=self.gui_osc_port,
size=(0, 10),
tooltip="OSC port we send data to.",
),
],
[
sg.Text("Receive functions", background_color=BACKGROUND_COLOR),
sg.Checkbox(
"",
default=self.config.gui_ROSC,
key=self.gui_ROSC,
background_color=BACKGROUND_COLOR,
size=(0, 10),
tooltip="Toggle OSC receive functions.",
),
],
[
sg.Text("Receiver Port:", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_osc_receiver_port,
key=self.gui_osc_receiver_port,
size=(0, 10),
tooltip="Port we receive OSC data from (used to recalibrate or recenter app from within VRChat.",
),
sg.Text("Recenter Address:", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_osc_recenter_address,
key=self.gui_osc_recenter_address,
size=(0, 10),
tooltip="OSC Address used for recentering your eye.",
),
],
[
sg.Text("Recalibrate Address:", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_osc_recalibrate_address,
key=self.gui_osc_recalibrate_address,
size=(0, 10),
tooltip="OSC address we use for recalibrating your eye",
),
],
]

View File

@ -0,0 +1,37 @@
from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel
from settings.constants import BACKGROUND_COLOR
import PySimpleGUI as sg
class OneEuroFilterValidationModel(BaseValidationModel):
gui_speed_coefficient: str # GUI lib does not support doubles ;-;
gui_min_cutoff: str # or floats ;-;
class OneEuroSettingsModule(BaseSettingsModule):
def __init__(self, config, widget_id, settings_base_class, **kwargs):
super().__init__(config, widget_id, settings_base_class, **kwargs)
self.gui_speed_coefficient = f"-SPEEDCOEFFICIENT{widget_id}-"
self.gui_min_cutoff = f"-MINCUTOFF{widget_id}-"
self.validation_model = OneEuroFilterValidationModel
def get_layout(self):
return [
[
sg.Text("One Euro Filter Paramaters:", background_color='#242224'),
],
[
sg.Text("Min Frequency Cutoff", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_min_cutoff,
key=self.gui_min_cutoff,
size=(0, 10),
),
sg.Text("Speed Coefficient", background_color=BACKGROUND_COLOR),
sg.InputText(
self.config.gui_speed_coefficient,
key=self.gui_speed_coefficient,
size=(0, 10),
),
],
]