"""Callback functions & State info for Sewing Pattern Configurator """ # NOTE: NiceGUI reference: https://nicegui.io/ import asyncio import shutil import traceback from argparse import Namespace # Async execution of regular functions from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path from typing import List, Tuple from uuid import uuid4 import numpy as np import yaml from nicegui import ui, app, events # Custom from .gui_pattern import GUIPattern icon_github = """ """ icon_arxiv = """""" # # Dracula color theme # theme_colors = Namespace( # primary='#BD93F9', # Light purple (primary accent in Dracula theme) # secondary='#FF79C6', # Pink # accent='#FFB86C', # Orange # dark='#282A36', # Dracula background (dark) # positive='#50FA7B', # Green # negative='#FF5555', # Red # info='#8BE9FD', # Cyan (info color) # warning='#F1FA8C' # Yellow # ) theme_colors = Namespace( primary='#5C5D71', secondary='#333333', accent='#a82c64', dark='#4d1f48', positive='#22ba38', negative='#f50000', info='#31CCEC', warning='#9333ea' ) messages: List[Tuple[str, str, str, str, str]] = [] @ui.refreshable def chat_messages(own_id: str) -> None: if messages: for user_id, avatar, text, stamp, img_url in messages: if img_url: with ui.chat_message(text=text, stamp=stamp, avatar=avatar, sent=own_id==user_id).classes('w-full'): ui.image(img_url).classes('w-64') else: ui.chat_message(text=text, stamp=stamp, avatar=avatar, sent=own_id==user_id).classes('w-full') else: ui.label('').classes('mx-auto my-36') ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)') # State of GUI class GUIState: """State of GUI-related objects NOTE: "#" is used as a separator in GUI keys to avoid confusion with symbols that can be (typically) used in body/design parameter names ('_', '-', etc.) """ def __init__(self) -> None: self.window = None # Pattern self.pattern_state = GUIPattern() # Pattern display constants self.canvas_aspect_ratio = 1500. / 900 # Millimiter paper self.w_rel_body_size = 0.5 # Body size as fraction of horisontal canvas axis self.h_rel_body_size = 0.95 self.background_body_scale = 1 / 171.99 # Inverse of the mean_all body height from GGG self.background_body_canvas_center = 0.273 # Fraction of the canvas (millimiter paper) self.w_canvas_pad, self.h_canvas_pad = 0.011, 0.04 self.body_outline_classes = '' # Application of pattern&body scaling when it overflows # Paths setup # Static images for GUI self.path_static_img = '/img' app.add_static_files(self.path_static_img, './assets/img') # 3D updates self.path_static_3d = '/geo' self.garm_3d_filename = f'garm_3d_{self.pattern_state.id}.glb' self.local_path_3d = Path('./tmp_gui/garm_3d') self.local_path_3d.mkdir(parents=True, exist_ok=True) app.add_static_files(self.path_static_3d, self.local_path_3d) app.add_static_files('/body', './assets/bodies') #model api self.baseurl = None self.api = None self.model = None self.text_model = None # Elements self.ui_design_subtabs = {} self.ui_pattern_display = None self._async_executor = ThreadPoolExecutor(1) self.pattern_state.reload_garment() self.stylings() self.layout() def release(self): """Clean-up after the sesssion""" self.pattern_state.release() (self.local_path_3d / self.garm_3d_filename).unlink(missing_ok=True) # Initial definitions def stylings(self): """Theme definition""" # Theme # Here: https://quasar.dev/style/theme-builder ui.colors( primary=theme_colors.primary, secondary=theme_colors.secondary, accent=theme_colors.accent, dark=theme_colors.dark, positive=theme_colors.positive, negative=theme_colors.negative, info=theme_colors.info, warning=theme_colors.warning ) # SECTION Top level layout def layout(self): """Overall page layout""" # as % of viewport width/height self.h_header = 5 self.h_params_content = 88 self.h_garment_display = 74 self.w_garment_display = 65 self.w_splitter_design = 32 self.scene_base_resoltion = (1024, 800) # Helpers self.def_pattern_waiting() # TODOLOW One dialog for both? self.def_design_file_dialog() self.def_body_file_dialog() # Configurator GUI with ui.row(wrap=False).classes(f'w-full h-[{self.h_params_content}dvh] p-0 m-0 '): # Tabs self.def_param_tabs_layout() # Pattern visual self.view_tabs_layout() # Overall wrapping # NOTE: https://nicegui.io/documentation/section_pages_routing#page_layout with ui.header(elevated=True, fixed=False).classes(f'h-[{self.h_header}vh] items-center justify-end py-0 px-4 m-0'): ui.label('Design2GarmentCode').classes('mr-auto').style('font-size: 150%; font-weight: 400') ui.button( 'Project', on_click=lambda: ui.navigate.to('https://style3d.github.io/design2garmentcode/', new_tab=True) ).props('flat color=white') with ui.link(target='http://arxiv.org/abs/2412.08603', new_tab=True): ui.html(icon_arxiv).classes('w-16 bg-transparent') with ui.link(target='https://github.com/Style3D/SXDGarmentCode', new_tab=True): ui.html(icon_github).classes('w-8 bg-transparent') # NOTE No ui.left_drawer(), no ui.right_drawer() with ui.footer(fixed=False, elevated=True).classes('items-center justify-center p-0 m-0'): # https://www.termsfeed.com/blog/sample-copyright-notices/ ui.link( '© 2025 Style3D Research', 'https://www.linctex.com/', new_tab=True ).classes('text-white') def view_tabs_layout(self): """2D/3D view tabs""" with ui.column(wrap=False).classes(f'h-[{self.h_params_content}vh] w-full items-center'): with ui.tabs() as tabs: self.ui_2d_tab = ui.tab('Sewing Pattern') self.ui_3d_tab = ui.tab('3D view') with ui.tab_panels(tabs, value=self.ui_2d_tab, animated=True).classes('w-full h-full items-center'): with ui.tab_panel(self.ui_2d_tab).classes('w-full h-full items-center justify-center p-0 m-0'): self.def_pattern_display() with ui.tab_panel(self.ui_3d_tab).classes('w-full h-full items-center p-0 m-0'): self.def_3d_scene() ui.button('Download Current Garment', on_click=lambda: self.state_download()).classes('justify-self-end') def def_param_tabs_layout(self): """Layout of tabs with parameters""" with ui.column(wrap=False).classes(f'h-[{self.h_params_content}vh]'): with ui.tabs().classes('w-full').props('dense') as tabs: self.ui_lmm_tab = ui.tab('Parse Design').props('icon=chat').classes('text-sm') self.ui_design_tab = ui.tab('Design Parameters').props('icon=checkroom').classes('text-sm') self.ui_body_tab = ui.tab('Body Measurement').props('icon=accessibility_new').classes('text-sm') with ui.tab_panels(tabs, value=self.ui_design_tab, animated=True).classes('w-full h-full items-center'): with ui.tab_panel(self.ui_lmm_tab).classes('w-full h-full items-center p-0 m-0'): self.def_lmm_tab() with ui.tab_panel(self.ui_design_tab).classes('w-full h-full items-center p-0 m-0'): self.def_design_tab() with ui.tab_panel(self.ui_body_tab).classes('w-full h-full items-center p-0 m-0'): self.def_body_tab() def def_lmm_tab(self): self.own_id = str(uuid4()) self.chat_msg = '' self.chat_img_url = '' def toggle_inputs(): if 'hidden' in self.api_input_fields.classes: self.api_input_fields.classes.remove('hidden') # display else: self.api_input_fields.classes.append('hidden') # hidden self.api_input_fields.update() # Update the UI def submit_api_data(): self.baseurl = self.baseurl_input.value self.api = self.api_input.value self.model = self.model_input.value self.text_model = self.text_model_input.value toggle_inputs() with ui.column().classes('w-full h-full'): with ui.element('div').classes('w-full h-full overflow-auto'): chat_messages(self.own_id) with ui.row().classes('w-full mt-2'): self.chat_input = ui.input( placeholder='Describe your design or upload a reference image...').props('autofocus').classes('flex-grow').on('keydown.enter', self.handle_chat_input) ui.button('Send', on_click=self.handle_chat_input).props('icon=send flat').classes('mt-2') with ui.column().classes('hidden').style('margin-left: auto;') as self.api_input_fields: self.baseurl_input = ui.input(placeholder='Base URL').classes('w-full') self.api_input = ui.input(placeholder='API Key').classes('w-full') self.model_input = ui.input(placeholder='Model Name').classes('w-full') self.text_model_input = ui.input(placeholder='Text Model Name').classes('w-full') ui.button('Submit', on_click=submit_api_data).classes('icon=send') ui.button('open/close input baseurl api model', on_click=toggle_inputs).classes('icon=send') with self.chat_input.add_slot('prepend'): ui.icon('add_photo_alternate').on('click', self.open_image_upload_dialog).classes('cursor-pointer').style('margin-left: 8px;') with ui.dialog() as self.image_upload_dialog: with ui.card(): ui.upload(on_upload=self.handle_image_upload).props('accept="image/*"').classes('w-full') ui.button('Close', on_click=self.image_upload_dialog.close).classes('mt-2') async def handle_chat_input(self, e=None): self.chat_msg = self.chat_input.value.strip() if self.chat_msg: timestamp = datetime.now().strftime('%H:%M') messages.append((self.own_id, f'{self.path_static_img}/artist.png', self.chat_msg, timestamp, self.chat_img_url)) chat_messages.refresh(self.own_id) await self.parse_design(self.chat_msg, self.chat_img_url,api_key=self.api, base_url=self.baseurl, model=self.model,text_model=self.text_model) self.chat_input.value = '' self.chat_img_url = '' def open_image_upload_dialog(self): self.image_upload_dialog.open() async def handle_image_upload(self, e: events.UploadEventArguments): image_name = e.name image_path = f'{self.path_static_img}/{image_name}' with open(f'./assets/img/{image_name}', 'wb') as f: f.write(e.content.read()) self.chat_img_url = image_path self.image_upload_dialog.close() self.chat_msg = self.chat_input.value.strip() timestamp = datetime.now().strftime('%H:%M') messages.append((self.own_id, f'{self.path_static_img}/artist.png', self.chat_msg, timestamp, self.chat_img_url)) chat_messages.refresh(self.own_id) await self.parse_design(self.chat_msg, f'./assets/img/{image_name}',api_key=self.api, base_url=self.baseurl, model=self.model,text_model=self.text_model) self.chat_input.value = '' self.chat_img_url = '' def def_body_tab(self): # Set of buttons with ui.row(): ui.button('Upload', on_click=self.ui_body_dialog.open) self.ui_active_body_refs = {} self.ui_passive_body_refs = {} with ui.scroll_area().classes('w-full h-full p-0 m-0'): # NOTE: p-0 m-0 gap-0 dont' seem to have effect body = self.pattern_state.body_params for param in body: param_name = param.replace('_', ' ').capitalize() elem = ui.number( label=param_name, value=str(body[param]), format='%.2f', precision=2, step=0.5, ).classes('text-[0.85rem]') if param[0] == '_': # Info elements for calculatable parameters elem.disable() self.ui_passive_body_refs[param] = elem else: # active elements accepting input # NOTE: e.sender == UI object, e.value == new value elem.on_value_change(lambda e, dic=body, param=param: self.update_pattern_ui_state( dic, param, e.value, body_param=True )) self.ui_active_body_refs[param] = elem def def_flat_design_subtab(self, ui_elems, design_params, use_collapsible=False): """Group of design parameters""" for param in design_params: param_name = param.replace('_', ' ').capitalize() if 'v' not in design_params[param]: ui_elems[param] = {} if use_collapsible: with ui.expansion().classes('w-full p-0 m-0') as expansion: with expansion.add_slot('header'): ui.label(f'{param_name}').classes('text-base self-center w-full h-full p-0 m-0') with ui.row().classes('w-full h-full p-0 m-0'): # Ensures correct application of style classes for children self.def_flat_design_subtab(ui_elems[param], design_params[param]) else: with ui.card().classes('w-full shadow-md border m-0 rounded-md'): ui.label(f'{param_name}').classes('text-base self-center w-full h-full p-0 m-0') self.def_flat_design_subtab(ui_elems[param], design_params[param]) else: # Leaf value p_type = design_params[param]['type'] val = design_params[param]['v'] p_range = design_params[param]['range'] if 'select' in p_type: values = design_params[param]['range'] if 'null' in p_type and None not in values: values.append(None) # NOTE: Displayable value ui.label(param_name).classes('p-0 m-0 mt-2 text-stone-500 text-[0.85rem]') ui_elems[param] = ui.select( values, value=val, on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value) ).classes('w-full') elif p_type == 'bool': ui_elems[param] = ui.switch( param_name, value=val, on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value) ).classes('text-stone-500') elif p_type == 'float' or p_type == 'int': ui.label(param_name).classes('p-0 m-0 mt-2 text-stone-500 text-[0.85rem]') ui_elems[param] = ui.slider( value=val, min=p_range[0], max=p_range[1], step=0.025 if p_type == 'float' else 1, ).props('snap label').classes('w-full') \ .on('update:model-value', lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.args), throttle=0.5, leading_events=False) # NOTE Events control: https://nicegui.io/documentation/slider#throttle_events_with_leading_and_trailing_options elif 'file' in p_type: print(f'GUI::NotImplementedERROR::{param}::' '"file" parameter type is not yet supported in Web GarmentCode. ' 'Creation of corresponding UI element skipped' ) else: print(f'GUI::WARNING::Unknown parameter type: {p_type}') ui_elems[param] = ui.input(label=param_name, value=val, placeholder='Type the value', validation={'Input too long': lambda value: len(value) < 20}, on_change=lambda e, dic=design_params, param=param: self.update_pattern_ui_state(dic, param, e.value) ).classes('w-full') def def_design_tab(self): # Set of buttons with ui.row(): ui.button('Random', on_click=self.random) ui.button('Default', on_click=self.default) ui.button('Upload', on_click=self.ui_design_dialog.open) # Design parameters design_params = self.pattern_state.design_params self.ui_design_refs = {} if self.pattern_state.is_design_sectioned(): # Use tabs to represent top-level sections with ui.splitter(value=self.w_splitter_design).classes('w-full h-full p-0 m-0') as splitter: with splitter.before: with ui.tabs().props('vertical').classes('w-full h-full') as tabs: for param in design_params: # Tab self.ui_design_subtabs[param] = ui.tab(param) self.ui_design_refs[param] = {} with splitter.after: with ui.tab_panels(tabs, value=self.ui_design_subtabs['meta']).props('vertical').classes('w-full h-full p-0 m-0'): for param, tab_elem in self.ui_design_subtabs.items(): with ui.tab_panel(tab_elem).classes('w-full h-full p-0 m-0').style('gap: 0px'): with ui.scroll_area().classes('w-full h-full p-0 m-0').style('gap: 0px'): self.def_flat_design_subtab( self.ui_design_refs[param], design_params[param], use_collapsible=(param == 'left') ) else: # Simplified display of designs with ui.scroll_area().classes('w-full h-full p-0 m-0'): self.def_flat_design_subtab( self.ui_design_refs, design_params, use_collapsible=True ) # !SECTION # SECTION -- Pattern visuals def def_pattern_display(self): """Prepare pattern display area""" with ui.column().classes('h-full p-0 m-0'): with ui.row().classes('w-full p-0 m-0 justify-between'): switch = ui.switch( 'Body Silhouette', value=True, ).props('dense left-label').classes('text-stone-800') self.ui_self_intersect = ui.label( 'WARNING: Garment panels are self-intersecting!' ).classes('font-semibold text-purple-600 border-purple-600 border py-0 px-1.5 rounded-md') \ .bind_visibility(self.pattern_state, 'is_self_intersecting') with ui.image( f'{self.path_static_img}/millimiter_paper_1500_900.png' ).classes(f'aspect-[{self.canvas_aspect_ratio}] h-[95%] p-0 m-0') as self.ui_pattern_bg: # NOTE: Positioning: https://github.com/zauberzeug/nicegui/discussions/957 with ui.row().classes('w-full h-full p-0 m-0 bg-transparent relative top-[0%] left-[0%]'): self.body_outline_classes = 'bg-transparent h-full absolute top-[0%] left-[0%] p-0 m-0' try: self.ui_body_outline = ui.image(f'{self.path_static_img}/ggg_outline_{self.pattern_state.body_id}.svg').classes(replace=self.body_outline_classes) switch.bind_value(self.ui_body_outline, 'visible') except Exception as e: print('GUI::WARNING::Body silhouette not found, using mean_all', str(e)) self.ui_body_outline = ui.image(f'{self.path_static_img}/ggg_outline_mean_all.svg').classes(self.body_outline_classes) switch.bind_value(self.ui_body_outline, 'visible') # NOTE: ui.row allows for correct classes application (e.g. no padding on svg pattern) with ui.row().classes('w-full h-full p-0 m-0 bg-transparent relative'): # Automatically updates from source self.ui_pattern_display = ui.interactive_image( '' ).classes('bg-transparent p-0 m-0') # !SECTION # SECTION 3D view def create_lights(self, scene:ui.scene, intensity=30.0): light_positions = np.array([ [1.60614, 1.23701, 1.5341,], [1.31844, -2.52238, 1.92831], [-2.80522, 2.34624, 1.2594], [0.160261, 3.52215, 1.81789], [-2.65752, -1.26328, 1.41194] ]) light_colors = [ '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff' ] z_dirs = np.arctan2(light_positions[:, 1], light_positions[:, 0]) # Add lights to the scene for i in range(len(light_positions)): scene.spot_light( color=light_colors[i], intensity=intensity, angle=np.pi, ).rotate(0., 0., -z_dirs[i]).move(light_positions[i][0], light_positions[i][1], light_positions[i][2]) def create_camera(self, cam_location, fov, scale=1.): camera = ui.scene.perspective_camera(fov=fov) camera.x = cam_location[0] * scale camera.y = cam_location[1] * scale camera.z = cam_location[2] * scale # direction camera.look_at_x = 0 camera.look_at_y = 0 camera.look_at_z = cam_location[2] * scale * 2/3 return camera def def_3d_scene(self): y_fov = 30 # Degrees == np.pi / 6. rad FOV camera_location = [0, -4.15, 1.25] bg_color='#ffffff' def body_visibility(value): self.ui_body_3d.visible(value) with ui.row().classes('w-full p-0 m-0 justify-between items-center'): self.ui_body_3d_switch = ui.switch( 'Body Silhouette', value=True, on_change=lambda e: body_visibility(e.value) ).props('dense left-label').classes('text-stone-800') ui.button('Drape current design', on_click=lambda: self.update_3d_scene()) ui.label( 'INFO: it takes a few minutes' ).classes(f'font-semibold text-[{theme_colors.primary}] border-[{theme_colors.primary}] ' 'border py-0 px-1.5 rounded-md') camera = self.create_camera(camera_location, y_fov) with ui.scene( width=self.scene_base_resoltion[0], height=self.scene_base_resoltion[1], camera=camera, grid=False, background_color=bg_color ).classes(f'w-[{self.w_garment_display}vw] h-[90%] p-0 m-0') as self.ui_3d_scene: # Lights setup self.create_lights(self.ui_3d_scene, intensity=60.) # NOTE: texture is there, just needs a better setup self.ui_garment_3d = None # TODOLOW Update body model to a correct shape try: self.ui_body_3d = self.ui_3d_scene.stl( f'/body/{self.pattern_state.body_id}.stl' ).rotate(np.pi / 2, 0., 0.).material(color='#000000') except Exception as e: print(str(e)) self.ui_body_3d = self.ui_3d_scene.stl( '/body/mean_all.stl' ).rotate(np.pi / 2, 0., 0.).material(color='#000000') # !SECTION # SECTION -- Other UI details def def_pattern_waiting(self): """Define the waiting splashcreen with spinner (e.g. waiting for a pattern to update)""" # NOTE: the screen darkens because of the shadow with ui.dialog(value=False).props( 'persistent maximized' ) as self.spin_dialog, ui.card().classes('bg-transparent'): # Styles https://quasar.dev/vue-components/spinners ui.spinner('hearts', size='15em').classes('fixed-center') # NOTE: 'dots' 'ball' def def_body_file_dialog(self): """ Dialog for loading parameter files (body) """ async def handle_upload(e: events.UploadEventArguments): param_dict = yaml.safe_load(e.content.read())['body'] self.toggle_param_update_events(self.ui_active_body_refs) self.pattern_state.body_id = e.name.split('.')[0] self.pattern_state.set_new_body_params(param_dict) self.update_body_params_ui_state(self.ui_active_body_refs) await self.update_pattern_ui_state() if self.ui_body_3d is not None: self.ui_body_3d.delete() try: print(f'INFO::Body silhouette update ggg_outline_{self.pattern_state.body_id}.svg') self.ui_body_3d = self.ui_3d_scene.stl( f'/body/{self.pattern_state.body_id}.stl' ).rotate(np.pi / 2, 0., 0.).material(color='#000000') except Exception as e: print('GUI::WARNING::3D Body mesh not found, using mean_all', str(e)) self.ui_body_3d = self.ui_3d_scene.stl( '/body/mean_all.stl' ).rotate(np.pi / 2, 0., 0.).material(color='#000000') try: print(f'INFO::Body silhouette update ggg_outline_{self.pattern_state.body_id}.svg') print('*** img_url: ', self.ui_body_outline.source) self.ui_body_outline.set_source(f'{self.path_static_img}/ggg_outline_{self.pattern_state.body_id}.svg') self.ui_body_outline.update() except Exception as e: print('GUI::WARNING::Body silhouette not found, using mean_all', str(e)) self.toggle_param_update_events(self.ui_active_body_refs) ui.notify(f'Successfully applied {e.name}') self.ui_body_dialog.close() with ui.dialog() as self.ui_body_dialog, ui.card().classes('items-center'): # NOTE: https://www.reddit.com/r/nicegui/comments/1393i2f/file_upload_with_restricted_types/ ui.upload( label='Body parameters .yaml or .json', on_upload=handle_upload ).classes('max-w-full').props('accept=".yaml,.json"') ui.button('Close without upload', on_click=self.ui_body_dialog.close) def def_design_file_dialog(self): """ Dialog for loading parameter files (design) """ async def handle_upload(e: events.UploadEventArguments): param_dict = yaml.safe_load(e.content.read())['design'] self.toggle_param_update_events(self.ui_design_refs) # Don't react to value updates self.pattern_state.set_new_design(param_dict) self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params) await self.update_pattern_ui_state() self.toggle_param_update_events(self.ui_design_refs) # Re-enable reaction to value updates ui.notify(f'Successfully applied {e.name}') self.ui_design_dialog.close() with ui.dialog() as self.ui_design_dialog, ui.card().classes('items-center'): # NOTE: https://www.reddit.com/r/nicegui/comments/1393i2f/file_upload_with_restricted_types/ ui.upload( label='Design parameters .yaml or .json', on_upload=handle_upload ).classes('max-w-full').props('accept=".yaml,.json"') ui.button('Close without upload', on_click=self.ui_design_dialog.close) # !SECTION # SECTION -- Event callbacks async def update_pattern_ui_state(self, param_dict=None, param=None, new_value=None, body_param=False): """UI was updated -- update the state of the pattern parameters and visuals""" # NOTE: Fixing to the "same value" issue in lambdas # https://github.com/zauberzeug/nicegui/wiki/FAQs#why-have-all-my-elements-the-same-value print('INFO::Updating pattern...') # Update the values if param_dict is not None: if body_param: param_dict[param] = new_value else: param_dict[param]['v'] = new_value self.pattern_state.is_in_3D = False # Design param changes -> 3D model is not synced with the param try: if not self.pattern_state.is_slow_design(): # Quick update self._sync_update_state() return # Display waiting spinner untill getting the result # NOTE Splashscreen solution to block users from modifying params while updating # https://github.com/zauberzeug/nicegui/discussions/1988 self.spin_dialog.open() # NOTE: Using threads for async call # https://stackoverflow.com/questions/49822552/python-asyncio-typeerror-object-dict-cant-be-used-in-await-expression self.loop = asyncio.get_event_loop() await self.loop.run_in_executor(self._async_executor, self._sync_update_state) self.spin_dialog.close() except KeyboardInterrupt as e: raise e except BaseException as e: traceback.print_exc() print(e) self.spin_dialog.close() # If open ui.notify( 'Failed to generate pattern correctly. Try different parameter values', type='negative', close_button=True, position='center' ) def _sync_update_state(self): # Update derivative body values (just in case) # TODOLOW only do that on body value updates self.pattern_state.body_params.eval_dependencies() self.update_body_params_ui_state(self.ui_passive_body_refs) # Display evaluated dependencies # Update the garment # Sync left-right for easier editing self.pattern_state.sync_left(with_check=True) # NOTE This is the slow part self.pattern_state.reload_garment() # TODOLOW the pattern is floating around when collars are added.. # Update display if self.ui_pattern_display is not None: if self.pattern_state.svg_filename: # Re-align the canvas and body with the new pattern p_bbox_size = self.pattern_state.svg_bbox_size p_bbox = self.pattern_state.svg_bbox # Margin calculations w.r.t. canvas size # s.t. the pattern scales correctly w_shift = abs(p_bbox[0]) # Body feet location in width direction w.r.t top-left corner of the pattern m_top = (1. - abs(p_bbox[2]) * self.background_body_scale) * self.h_rel_body_size + (1. - self.h_rel_body_size) / 2 m_left = self.background_body_canvas_center - w_shift * self.background_body_scale * self.w_rel_body_size m_right = 1 - m_left - p_bbox_size[0] * self.background_body_scale * self.w_rel_body_size m_bottom = 1 - m_top - p_bbox_size[1] * self.background_body_scale * self.h_rel_body_size # Canvas padding adjustment m_top -= self.h_canvas_pad m_left -= self.w_canvas_pad m_right += self.w_canvas_pad # preserve evaluated width m_bottom -= self.h_canvas_pad # New placement if m_top < 0 or m_bottom < 0 or m_left < 0 or m_right < 0: # Calculate the fraction scale_margin = 1.2 y_top_scale = abs(min(m_top * scale_margin, 0.)) + 1. y_bot_scale = 1. + abs(min(m_bottom * scale_margin, 0.)) x_left_scale = abs(min(m_left * scale_margin, 0.)) + 1. x_right_scale = abs(min(m_right * scale_margin, 0.)) + 1. scale = min(1. / y_top_scale, 1. / y_bot_scale, 1. / x_left_scale, 1. / x_right_scale) # Rescale the body self.ui_body_outline.classes( replace=self.body_outline_classes + f' origin-center scale-[{scale}]' ) # Recalculate positioning & width body_center = 0.5 - self.background_body_canvas_center m_top = (1. - abs(p_bbox[2]) * self.background_body_scale) * self.h_rel_body_size * scale + (1. - self.h_rel_body_size * scale) / 2 m_left = (0.5 - body_center * scale) - w_shift * self.background_body_scale * self.w_rel_body_size * scale m_right = 1 - m_left - p_bbox_size[0] * self.background_body_scale * self.w_rel_body_size * scale # Canvas padding adjustment # TODOLOW For some reason top adjustment is not needed here: m_top -= self.h_canvas_pad * scale m_left -= self.w_canvas_pad * scale m_right += self.w_canvas_pad * scale else: # Display normally # Remove body transforms if any were applied self.ui_body_outline.classes(replace=self.body_outline_classes) # New pattern image self.ui_pattern_display.set_source( str(self.pattern_state.svg_path()) if self.pattern_state.svg_filename else '') self.ui_pattern_display.classes( replace=f"""bg-transparent p-0 m-0 absolute left-[{m_left * 100}%] top-[{m_top * 100}%] w-[{(1. - m_right - m_left) * 100}%] height-auto """) else: # Restore default state self.ui_pattern_display.set_source('') self.ui_body_outline.classes(replace=self.body_outline_classes) def update_design_params_ui_state(self, ui_elems, design_params): """Sync ui params with the current state of the design params""" for param in design_params: if 'v' not in design_params[param]: self.update_design_params_ui_state(ui_elems[param], design_params[param]) else: ui_elems[param].value = design_params[param]['v'] def toggle_param_update_events(self, ui_elems): """Enable/disable event handling on the ui elements related to GarmentCode parameters""" for param in ui_elems: if isinstance(ui_elems[param], dict): self.toggle_param_update_events(ui_elems[param]) else: if ui_elems[param].is_ignoring_events: # -> disabled ui_elems[param].enable() else: ui_elems[param].disable() def update_body_params_ui_state(self, ui_body_refs): """Sync ui params with the current state of the body params""" for param in ui_body_refs: ui_body_refs[param].value = self.pattern_state.body_params[param] async def update_3d_scene(self): """According the whatever pattern current state""" print('INFO::Updating 3D...') # Cleanup if self.ui_garment_3d is not None: self.ui_garment_3d.delete() self.ui_garment_3d = None if not self.pattern_state.svg_filename: print('INFO::Current garment is empty, skipped 3D update') ui.notify('Current garment is empty. Chose a design to start simulating!') self.ui_body_3d.visible(True) self.ui_body_3d_switch.set_value(True) return try: # Display waiting spinner untill getting the result # NOTE Splashscreen solution to block users from modifying params while updating # https://github.com/zauberzeug/nicegui/discussions/1988 self.spin_dialog.open() # NOTE: Using threads for async call # https://stackoverflow.com/questions/49822552/python-asyncio-typeerror-object-dict-cant-be-used-in-await-expression self.loop = asyncio.get_event_loop() await self.loop.run_in_executor(self._async_executor, self._sync_update_3d) # Update ui # https://github.com/zauberzeug/nicegui/discussions/1269 with self.ui_3d_scene: # NOTE: material is defined in the glb file self.ui_garment_3d = self.ui_3d_scene.gltf( f'geo/{self.garm_3d_filename}', ).scale(0.01).rotate(np.pi / 2, 0., 0.) # Show the result! =) self.spin_dialog.close() except KeyboardInterrupt as e: raise e except BaseException as e: traceback.print_exc() print(e) self.ui_3d_scene.set_visibility(True) self.spin_dialog.close() # If open ui.notify( 'Failed to generate 3D model correctly. Try different parameter values', type='negative', close_button=True, position='center' ) def _sync_update_3d(self): """Update 3d model""" # Run simulation path, filename = self.pattern_state.drape_3d() # NOTE: The files will be available publically at the static point # However, we cannot do much about it, since it won't be available for the interface otherwise shutil.copy2(path / filename, self.local_path_3d / self.garm_3d_filename) # Design buttons updates async def design_sample(self): """Run design sampling""" self.loop = asyncio.get_event_loop() await self.loop.run_in_executor(self._async_executor, self.pattern_state.sample_design) async def random(self): # Sampling could be slow, so add spin always self.spin_dialog.open() self.toggle_param_update_events(self.ui_design_refs) # Don't react to value updates await self.design_sample() self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params) await self.update_pattern_ui_state() self.toggle_param_update_events(self.ui_design_refs) # Re-do reaction to value updates self.spin_dialog.close() async def default(self): self.toggle_param_update_events(self.ui_design_refs) self.pattern_state.restore_design(False) self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params) await self.update_pattern_ui_state() self.toggle_param_update_events(self.ui_design_refs) async def parse_design(self, text_prompt='', img_url='',api_key=None, base_url=None, model=None,text_model=None): """Parse design from text or image""" def _sync_parse_design(): response = self.pattern_state.parse_chat(text_prompt, img_url,api_key, base_url, model,text_model) return response self.spin_dialog.open() self.toggle_param_update_events(self.ui_design_refs) self.loop = asyncio.get_event_loop() response = await self.loop.run_in_executor(self._async_executor, _sync_parse_design) # response = self.pattern_state.parse_chat(text_prompt, img_url) print('GPT response: ', response) self.update_design_params_ui_state(self.ui_design_refs, self.pattern_state.design_params) await self.update_pattern_ui_state() self.toggle_param_update_events(self.ui_design_refs) self.spin_dialog.close() # !SECTION def state_download(self): """Download current state of a garment""" archive_path = self.pattern_state.save() ui.download(archive_path, f'Configured_design_{datetime.now().strftime("%y%m%d-%H-%M-%S")}.zip')