init_code

This commit is contained in:
sky
2025-07-03 17:03:00 +08:00
parent a710c87a2b
commit 89766fe3d1
220 changed files with 479903 additions and 77 deletions

0
gui/__init__.py Normal file
View File

918
gui/callbacks.py Normal file
View File

@@ -0,0 +1,918 @@
"""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 = """
<svg viewbox="0 0 98 96" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0
21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362
0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015
4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283
0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571
12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015
13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89
2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/>
</svg>
"""
icon_arxiv = """<svg id="primary_logo_-_single_color_-_white" data-name="primary logo - single color - white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 246.978 110.119"><path d="M492.976,269.5l24.36-29.89c1.492-1.989,2.2-3.03,1.492-4.723a5.142,5.142,0,0,0-4.481-3.161h0a4.024,4.024,0,0,0-3.008,1.108L485.2,261.094Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M526.273,325.341,493.91,287.058l-.972,1.033-7.789-9.214-7.743-9.357-4.695,5.076a4.769,4.769,0,0,0,.015,6.53L520.512,332.2a3.913,3.913,0,0,0,3.137,1.192,4.394,4.394,0,0,0,4.027-2.818C528.4,328.844,527.6,327.133,526.273,325.341Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M479.215,288.087l6.052,6.485L458.714,322.7a2.98,2.98,0,0,1-2.275,1.194,3.449,3.449,0,0,1-3.241-2.144c-.513-1.231.166-3.15,1.122-4.168l.023-.024.021-.026,24.851-29.448m-.047-1.882-25.76,30.524c-1.286,1.372-2.084,3.777-1.365,5.5a4.705,4.705,0,0,0,4.4,2.914,4.191,4.191,0,0,0,3.161-1.563l27.382-29.007-7.814-8.372Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M427.571,255.154c1.859,0,3.1,1.24,3.985,3.453,1.062-2.213,2.568-3.453,4.694-3.453h14.878a4.062,4.062,0,0,1,4.074,4.074v7.828c0,2.656-1.327,4.074-4.074,4.074-2.656,0-4.074-1.418-4.074-4.074V263.3H436.515a2.411,2.411,0,0,0-2.656,2.745v27.188h10.007c2.658,0,4.074,1.329,4.074,4.074s-1.416,4.074-4.074,4.074h-26.39c-2.659,0-3.986-1.328-3.986-4.074s1.327-4.074,3.986-4.074h8.236V263.3h-7.263c-2.656,0-3.985-1.329-3.985-4.074,0-2.658,1.329-4.074,3.985-4.074Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M539.233,255.154c2.656,0,4.074,1.416,4.074,4.074v34.007h10.1c2.746,0,4.074,1.329,4.074,4.074s-1.328,4.074-4.074,4.074H524.8c-2.656,0-4.074-1.328-4.074-4.074s1.418-4.074,4.074-4.074h10.362V263.3h-8.533c-2.744,0-4.073-1.329-4.073-4.074,0-2.658,1.329-4.074,4.073-4.074Zm4.22-17.615a5.859,5.859,0,1,1-5.819-5.819A5.9,5.9,0,0,1,543.453,237.539Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M605.143,259.228a4.589,4.589,0,0,1-.267,1.594L590,298.9a3.722,3.722,0,0,1-3.721,2.48h-5.933a3.689,3.689,0,0,1-3.808-2.48l-15.055-38.081a3.23,3.23,0,0,1-.355-1.594,4.084,4.084,0,0,1,4.164-4.074,3.8,3.8,0,0,1,3.718,2.656l14.348,36.134,13.9-36.134a3.8,3.8,0,0,1,3.72-2.656A4.084,4.084,0,0,1,605.143,259.228Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M390.61,255.154c5.018,0,8.206,3.312,8.206,8.4v37.831H363.308a4.813,4.813,0,0,1-5.143-4.929V283.427a8.256,8.256,0,0,1,7-8.148l25.507-3.572v-8.4H362.306a4.014,4.014,0,0,1-4.141-4.074c0-2.87,2.143-4.074,4.355-4.074Zm.059,38.081V279.942l-24.354,3.4v9.9Z" transform="translate(-358.165 -223.27)" fill="#fff"/><path d="M448.538,224.52h.077c1,.024,2.236,1.245,2.589,1.669l.023.028.024.026,46.664,50.433a3.173,3.173,0,0,1-.034,4.336l-4.893,5.2-6.876-8.134L446.652,230.4c-1.508-2.166-1.617-2.836-1.191-3.858a3.353,3.353,0,0,1,3.077-2.02m0-1.25a4.606,4.606,0,0,0-4.231,2.789c-.705,1.692-.2,2.88,1.349,5.1l39.493,47.722,7.789,9.214,5.853-6.221a4.417,4.417,0,0,0,.042-6.042L452.169,225.4s-1.713-2.08-3.524-2.124Z" transform="translate(-358.165 -223.27)" fill="#fff"/></svg>"""
# # 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')

68
gui/error_pages.py Normal file
View File

@@ -0,0 +1,68 @@
from nicegui import ui, app
from nicegui import Client
from nicegui.page import page
import random
from gui.callbacks import theme_colors
# dresses selection =)
error_icons = [
'./assets/img/err_dress_20s.png',
'./assets/img/err_dress_30s.png',
'./assets/img/err_dress_50s.png',
'./assets/img/err_js.png',
'./assets/img/err_red_modern.png',
'./assets/img/err_regency.png'
]
# https://github.com/zauberzeug/nicegui/discussions/883#discussioncomment-5801636
def error_handler(err_type, text, exception: Exception):
"""Base error page, with customizable error messages"""
with ui.column().classes('h-[95vh] w-[95vw] items-center justify-top space-y-8 self-center'):
img = random.choice(error_icons)
ui.image(img).classes('h-[45vh]').props('fit="scale-down"')
with ui.column().classes('h-fit w-fit py-4 px-10 items-center justify-center space-y-8 '
f'border border-[{theme_colors.primary}] rounded-md '
f'shadow-lg shadow-[{theme_colors.secondary}]'):
ui.label(err_type).classes('text-3xl')
if text:
ui.label(text).classes('text-2xl')
ui.label(str(exception)).classes('text-xl text-stone-500')
# https://www.pixelfish.com.au/blog/most-common-website-errors/
@app.exception_handler(404)
async def exception_handler_404(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('404', 'You are looking for something that doesn\'t exist', exception)
return client.build_response(request, 404)
@app.exception_handler(500)
async def exception_handler_500(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('500', 'Oops! Server error. We are fixing it ASAP =)', exception)
return client.build_response(request, 500)
@app.exception_handler(400)
async def exception_handler_400(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('400', 'Oh no, bad request', exception)
return client.build_response(request, 400)
@app.exception_handler(401)
async def exception_handler_401(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('401', 'You don\'t have access to this place', exception)
return client.build_response(request, 401)
@app.exception_handler(403)
async def exception_handler_403(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('403', 'Sorry, you cannot come here', exception)
return client.build_response(request, 403)
@app.exception_handler(503)
async def exception_handler_503(request, exception: Exception):
with Client(page(''), request=None) as client:
error_handler('503', 'We are unavailable, but will be back soon!', exception)
return client.build_response(request, 503)

401
gui/gui_pattern.py Normal file
View File

@@ -0,0 +1,401 @@
from pathlib import Path
import time
import yaml
import shutil
import string
import random
import trimesh
from copy import deepcopy
from typing import Optional
from lmm_utils.core import MMUA
# Custom
from assets.garment_programs.meta_garment import MetaGarment
from assets.bodies.body_params import BodyParameters
import pygarment as pyg
from pygarment.meshgen.boxmeshgen import BoxMesh
# from pygarment.meshgen.simulation import run_sim
import pygarment.data_config as data_config
from pygarment.meshgen.sim_config import PathCofig
verbose = False
def _id_generator(size=10, chars=string.ascii_uppercase + string.digits):
"""Generate a random string of a given size, see
https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits
"""
return ''.join(random.choices(chars, k=size))
class GUIPattern:
def __init__(self) -> None:
# Unique id to distiguish tab sessions correctly
self.id = _id_generator(20)
# Paths setup
self.save_path_root = Path.cwd() / 'tmp_gui' / 'downloads'
self.tmp_path_root = Path.cwd() / 'tmp_gui' / 'display'
self.save_path = self.save_path_root / self.id
self.svg_filename = None
self.saved_garment_archive = ''
self.saved_garment_folder = ''
self.tmp_path = self.tmp_path_root / self.id
self.paths_3d = None
# create paths
self.save_path.mkdir(parents=True, exist_ok=True)
self.tmp_path.mkdir(parents=True, exist_ok=True)
self.body_params = None
self.design_params = {}
self.design_list = []
self.design_sampler = pyg.DesignSampler()
self.sew_pattern = None
self.body_id = 'mean_all'
self.body_file = None
self.design_file = None
self._load_body_file(
Path.cwd() / f'assets/bodies/{self.body_id}.yaml'
)
self.default_body_params = deepcopy(self.body_params)
self._load_design_file(
Path.cwd() / 'assets/design_params/default.yaml'
)
# Status
self.is_self_intersecting = False
self.is_in_3D = False
self.reload_garment()
# Init Agent
self.agent=None
def parse_chat(self, text_prompt='', img_url='',api_key=None, base_url=None, model=None,text_model=None):
print('Prompt: ', text_prompt)
print('Image: ', img_url)
self.agent.mmua=MMUA(api_key=api_key, base_url=base_url, model=model,text_model=text_model)
# self.agent=Agent(api_key=api_key, base_url=base_url, model=model,text_model=text_model)
text_prompt = text_prompt.strip()
modify_mode = 'modify' in text_prompt or 'm:' in text_prompt
stress_mode='stress' in text_prompt or 's:' in text_prompt
# try:
# Modify mode
if modify_mode:
assert self.design_params and self.design_list, "Must provide a base design under mofify mode."
gpt_response,gpt_design_params,gpt_design_list=self.agent.modify_design(self.design_list,text_prompt=text_prompt,design_params=self.design_params)
# Stress mode:
elif stress_mode:
assert self.design_params and self.design_list, "Must provide a base design under mofify mode."
gpt_response,gpt_design_params,gpt_design_list=self.agent.stress_design(self.design_list,img_url=img_url,design_params=self.design_params)
# Inference from image and text
elif text_prompt and img_url:
gpt_response,gpt_design_params,gpt_design_list=self.agent.picture_text_design(img_url,text_prompt)
# Inference from text only
elif text_prompt and not img_url:
gpt_response,gpt_design_params,gpt_design_list=self.agent.text_design(text_prompt)
# Inference from image only
elif img_url and not text_prompt:
gpt_response,gpt_design_params,gpt_design_list=self.agent.picture_design(img_url)
else:
raise ValueError("At least one design description is required, text prompt or image.")
self.design_list = gpt_design_list
self.set_new_design(gpt_design_params)
self.design_params=gpt_design_params
print('*** Design params: ', self.design_list, self.design_params)
return gpt_response
def release(self):
"""Clean up tmp files after the session"""
self.clear_previous_download()
shutil.rmtree(self.save_path)
shutil.rmtree(self.tmp_path)
def _load_body_file(self, path):
self.body_file = path
self.body_params = BodyParameters(path)
def _load_design_file(self, path):
self.design_file = path
# Create values
with open(path, 'r') as f:
des = yaml.safe_load(f)['design']
self.design_params.update(des)
if 'left' in self.design_params and not self.design_params['left']['enable_asym']['v']:
self.sync_left()
# Update param sampler
self.design_sampler.load(path)
def svg_path(self):
return self.tmp_path / self.svg_filename
def set_new_design(self, design):
self._nested_sync(design, self.design_params)
def set_new_body_params(self, body_params):
self.body_params.load_from_dict(body_params)
def sample_design(self, reload=True):
"""Random design parameters"""
new_design = self.design_sampler.randomize()
# NOTE: re-assign the values instead up overwriting them
self._nested_sync(new_design, self.design_params)
if 'left' in self.design_params and not self.design_params['left']['enable_asym']['v']:
self.sync_left()
if reload:
self.reload_garment()
def restore_design(self, reload=True):
"""Restore design values to match the current loaded file"""
new_design = self.design_sampler.default()
# re-assign the values instead up overwriting them
self._nested_sync(new_design, self.design_params)
if reload:
self.reload_garment()
def reload_garment(self):
"""Reload sewing pattern with current body and design parameters
NOTE: loading a pattern might be lagging, execute only when needed!
"""
self.sew_pattern = MetaGarment(
'Configured_design', self.body_params, self.design_params)
self.is_self_intersecting = self.sew_pattern.is_self_intersecting()
self._view_serialize()
@staticmethod
def _nested_sync(s_from, s_to):
if 'v' in s_to:
s_to['v'] = s_from['v']
else:
for key in s_to:
if key in s_from:
GUIPattern._nested_sync(s_from[key], s_to[key])
def sync_left(self, with_check=False):
"""Synchronize left and right design parameters"""
# Check if needed in the first place
if with_check and self.design_params['left']['enable_asym']['v']:
# Asymmetry enabled, the params should not syncronise
return
for k in self.design_params['left']:
if k != 'enable_asym':
# Use proper value assignment instead of deepcopy
self._nested_sync(self.design_params[k], self.design_params['left'][k])
def _view_serialize(self):
"""Save a sewing pattern svg representation to tmp folder be used
for display"""
# Get the flat representation
pattern = self.sew_pattern.assembly()
# Clear up the folder from previous version -- it's not needed any more
self.clear_previous_svg()
try:
self.svg_filename = f'pattern_{time.time()}.svg'
dwg = pattern.get_svg(self.tmp_path / self.svg_filename,
with_text=False,
view_ids=False,
flat=False,
margin=0
)
dwg.save()
self.svg_bbox_size = pattern.svg_bbox_size
self.svg_bbox = pattern.svg_bbox
except pyg.EmptyPatternError:
self.svg_filename = ''
# Cleaning
def clear_previous_svg(self):
"""Clear previous svg display file"""
if self.svg_filename:
(self.tmp_path / self.svg_filename).unlink()
self.svg_filename = ''
def clear_previous_download(self):
"""Clear previous download package display file"""
if self.saved_garment_folder:
shutil.rmtree(self.saved_garment_folder)
self.saved_garment_folder = ''
if self.saved_garment_archive:
self.saved_garment_archive.unlink()
self.saved_garment_archive = ''
def clear_3d(self):
if self.paths_3d is not None:
shutil.rmtree(self.paths_3d.out_el)
self.paths_3d = None
# 3D
def drape_3d(self):
"""Run the draping of the current frame"""
# Config setup
props = data_config.Properties('./assets/Sim_props/gui_sim_props.yaml') # TODOLOW Parameter?
props.set_section_stats('sim', fails={}, sim_time={}, spf={}, fin_frame={}, body_collisions={}, self_collisions={})
props.set_section_stats('render', render_time={})
# Force the design to be fitted to mean body shape
# TODOLOW Support body shape estimation from measurements
def_sew_pattern = MetaGarment(
'Configured_design', self.default_body_params, self.design_params)
# Save the pattern
pattern_folder = self.save(False, save_pattern=def_sew_pattern)
# Paths
paths = PathCofig(
in_element_path=pattern_folder,
out_path=self.save_path,
in_name=def_sew_pattern.name,
out_name=self.sew_pattern.name + '_3D',
body_name=self.body_id, # 'f_smpl_average_A40'
smpl_body=False, # NOTE: depends on chosen body model
add_timestamp=False
)
# Generate and save garment box mesh (if not existent)
garment_box_mesh = BoxMesh(paths.in_g_spec, props['sim']['config']['resolution_scale'])
garment_box_mesh.load()
garment_box_mesh.serialize(
paths, store_panels=False, uv_config=props['render']['config']['uv_texture'])
# TODOLOW Don't print progress to console with so many lines
run_sim(
garment_box_mesh.name,
props,
paths,
save_v_norms=False,
store_usd=False, # NOTE: False for fast simulation!,
optimize_storage=False,
verbose=False
)
# Convert to displayable element
mesh = trimesh.load_mesh(paths.g_sim)
# enable double-sided material for nice viewing
pbr_material = mesh.visual.material.to_pbr()
pbr_material.doubleSided = True
mesh.visual.material = pbr_material
# export
mesh.export(paths.g_sim_glb)
self.paths_3d = paths
self.is_in_3D = True
return paths.out_el, paths.g_sim_glb.name
# Current state
def is_design_sectioned(self):
"""Check if design parameters are grouped by sections:
the top level of design dictionary does not contain actual parameters
"""
for param in self.design_params:
if 'v' in self.design_params[param]:
return False
return True
def is_slow_design(self) -> bool:
"""Check is parameters that result in slow pattern generation are enabled
E.g. curved armhole evaluation
"""
# Pants
if (self.design_params['meta']['bottom']['v'] == 'Pants'):
return True
# Upper garment
is_not_upper = self.design_params['meta']['upper']['v'] is None
if is_not_upper:
return False
# Upper + fitted + strapless
is_asymm = self.design_params['left']['enable_asym']['v']
is_fitted = 'Fitted' in self.design_params['meta']['upper']['v']
is_strapless = self.design_params['fitted_shirt']['strapless']['v']
is_asymm_strapless = self.design_params['left']['fitted_shirt']['strapless']['v']
is_strapless = is_fitted and is_strapless
is_asymm_strapless = is_fitted and is_asymm_strapless
# Has a hoody
collar_component = self.design_params['collar']['component']['style']['v']
has_hoody = collar_component is not None and 'Hood' in collar_component
# Sleeve potential setup
sleeves = self.design_params['sleeve']
is_sleeveless = sleeves['sleeveless']['v']
is_curve = sleeves['armhole_shape']['v'] == 'ArmholeCurve'
is_curve = not is_sleeveless and is_curve
is_asym_sleeveless = self.design_params['left']['sleeve']['sleeveless']['v']
is_asymm_curve = self.design_params['left']['sleeve']['armhole_shape']['v'] == 'ArmholeCurve'
is_asymm_curve = not is_asym_sleeveless and is_asymm_curve
if is_asymm:
right_check = (not is_strapless) and is_curve
left_check = (not is_asymm_strapless) and is_asymm_curve
return right_check or left_check
else:
return (not is_strapless) and is_curve or has_hoody
def save(self, pack=True, save_pattern: Optional[MetaGarment]=None):
"""Save current garment design to self.save_path """
# Save current pattern
if save_pattern is None:
save_pattern = self.sew_pattern
pattern = save_pattern.assembly()
# Save as json file
self.saved_garment_folder = pattern.serialize(
self.save_path,
to_subfolder=True,
with_3d=False, with_text=False, view_ids=False,
with_printable=True,
empty_ok=True
)
self.saved_garment_folder = Path(self.saved_garment_folder)
self.body_params.save(self.saved_garment_folder)
with open(self.saved_garment_folder / 'design_params.yaml', 'w') as f:
yaml.dump(
{'design': self.design_params},
f,
default_flow_style=False,
sort_keys=False
)
# pack
if pack:
# Only add geometry if design didn't change since last drape
if not self.is_in_3D:
self.clear_3d() # Clean any saved 3D if it's not synced with current design
self.saved_garment_archive = Path(shutil.make_archive(
self.save_path / '..' / f'{self.saved_garment_folder.name}_{self.id}', 'zip',
root_dir=self.save_path
))
print(f'Success! {self.sew_pattern.name} saved to {self.saved_garment_folder}')
return self.saved_garment_archive if pack else self.saved_garment_folder

24
gui/maya_garmentviewer.py Normal file
View File

@@ -0,0 +1,24 @@
"""
Loads maya interface for editing & testing template files
* Maya 2022+
* Qualoth
"""
from maya import cmds
from importlib import reload
# My modules
from pygarment import mayaqltools as mymaya
reload(mymaya)
# -------------- Main -------------
if __name__ == "__main__":
print('Load plugins')
mymaya.qualothwrapper.load_plugin()
cmds.loadPlugin('mtoa.mll') # https://stackoverflow.com/questions/50422566/how-to-register-arnold-render
cmds.loadPlugin('objExport.mll') # same as in https://forums.autodesk.com/t5/maya-programming/invalid-file-type-specified-atomimport/td-p/9121166
try:
mymaya.garmentUI.start_GUI()
except Exception as e:
print(e)