Files
design2garmentcode-impl/gui/gui_pattern.py
2025-07-03 17:03:00 +08:00

402 lines
15 KiB
Python

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