import numpy as np from scipy.spatial.transform import Rotation as R import pygarment as pyg from assets.garment_programs.base_classes import StackableSkirtComponent from assets.garment_programs.base_classes import BaseBottoms from assets.garment_programs import shapes class SkirtPanel(pyg.Panel): """One panel of a panel skirt with ruffles on the waist""" def __init__(self, name, waist_length=70, length=70, ruffles=1, match_top_int_to=None, bottom_cut=0, flare=0 ) -> None: super().__init__(name) base_width = waist_length top_width = base_width * ruffles low_width = top_width + 2*flare x_shift_top = (low_width - top_width) / 2 # to account for flare at the bottom # define edge loop self.right = pyg.EdgeSeqFactory.side_with_cut( [0, 0], [x_shift_top, length], start_cut=bottom_cut / length) if bottom_cut else pyg.EdgeSequence( pyg.Edge([0, 0], [x_shift_top, length])) self.waist = pyg.Edge( self.right[-1].end, [x_shift_top + top_width, length]) self.left = pyg.EdgeSeqFactory.side_with_cut( self.waist.end, [low_width, 0], end_cut=bottom_cut / length) if bottom_cut else pyg.EdgeSequence( pyg.Edge(self.waist.end, [low_width, 0])) self.bottom = pyg.Edge(self.left[-1].end, self.right[0].start) # define interface self.interfaces = { 'right': pyg.Interface(self, self.right[-1]), 'top': pyg.Interface(self, self.waist, ruffle=self.waist.length() / match_top_int_to if match_top_int_to is not None else ruffles ).reverse(True), 'left': pyg.Interface(self, self.left[0]), 'bottom': pyg.Interface(self, self.bottom) } # Single sequence for correct assembly self.edges = self.right self.edges.append(self.waist) # on the waist self.edges.append(self.left) self.edges.append(self.bottom) # default placement self.top_center_pivot() self.center_x() # Already know that this panel should be centered over Y class ThinSkirtPanel(pyg.Panel): """One panel of a panel skirt""" def __init__(self, name, top_width=10, bottom_width=20, length=70, b_curvature=0) -> None: super().__init__(name) # define edge loop self.flare = (bottom_width - top_width) / 2 self.edges = pyg.EdgeSeqFactory.from_verts( [0, 0], [self.flare, length], [self.flare + top_width, length], [self.flare * 2 + top_width, 0]) if pyg.utils.close_enough(b_curvature, 0): self.edges.close_loop() else: self.edges.append( pyg.CircleEdgeFactory.from_three_points( self.edges[-1].end, self.edges[0].start, [0.5, b_curvature], relative=True ) ) # w.r.t. top left point self.set_pivot(self.edges[0].end) self.interfaces = { 'right': pyg.Interface(self, self.edges[0]), 'top': pyg.Interface(self, self.edges[1]), 'left': pyg.Interface(self, self.edges[2]), 'bottom': pyg.Interface(self, self.edges[-1]) } class FittedSkirtPanel(pyg.Panel): """Fitted panel for a pencil skirt""" def __init__( self, name, body, design, waist, hips, hips_depth, # TODOLOW Half measurement instead of a quarter length, hipline_ext=1, dart_position=None, dart_frac=0.5, double_dart=False, match_top_int_to=None, slit=0, left_slit=0, right_slit=0, side_cut=None, flip_side_cut=False ) -> None: """ Fitted panel for a pencil skirt Body/design values that differ between front and back panels are supplied as parameters, the rest are taken from the body and design dictionaries """ super().__init__(name) # Shared params low_angle = design['low_angle']['v'] hip_side_incl = np.deg2rad(body['_hip_inclination']) flare = design['flare']['v'] low_width = body['hips'] * (flare - 1) / 4 + hips # Distribute the difference equally # between front and back # adjust for a rise adj_hips_depth = hips_depth * hipline_ext dart_depth = hips_depth * dart_frac dart_depth = max(dart_depth - (hips_depth - adj_hips_depth), 0) # amount of extra fabric w_diff = hips - waist # Assume its positive since waist is smaller then hips # We distribute w_diff among the side angle and a dart hw_shift = np.tan(hip_side_incl) * adj_hips_depth # Small difference if hw_shift > w_diff: hw_shift = w_diff # Adjust the bottom edge to the desired angle angle_shift = np.tan(np.deg2rad(low_angle)) * low_width # --- Edges definition --- # Right if pyg.utils.close_enough(flare, 1): # skip optimization right_bottom = pyg.Edge( [hips - low_width, angle_shift], [0, length] ) else: right_bottom = pyg.CurveEdgeFactory.curve_from_tangents( [hips - low_width, angle_shift], [0, length], target_tan1=np.array([0, 1]), # initial guess places control point closer to the hips initial_guess=[0.75, 0] ) right_top = pyg.CurveEdgeFactory.curve_from_tangents( right_bottom.end, [hw_shift, length + adj_hips_depth], target_tan0=np.array([0, 1]), initial_guess=[0.5, 0] ) right = pyg.EdgeSequence(right_bottom, right_top) # top top = pyg.Edge(right[-1].end, [hips * 2 - hw_shift, length + adj_hips_depth]) # left left_top = pyg.CurveEdgeFactory.curve_from_tangents( top.end, [hips * 2, length], target_tan1=np.array([0, -1]), initial_guess=[0.5, 0] ) if pyg.utils.close_enough(flare, 1): # skip optimization for straight skirt left_bottom = pyg.Edge( left_top.end, [hips + low_width, -angle_shift], ) else: left_bottom = pyg.CurveEdgeFactory.curve_from_tangents( left_top.end, [hips + low_width, -angle_shift], target_tan0=np.array([0, -1]), # initial guess places control point closer to the hips initial_guess=[0.25, 0] ) left = pyg.EdgeSequence(left_top, left_bottom) # fin self.edges = pyg.EdgeSequence(right, top, left).close_loop() bottom = self.edges[-1] if slit: # add a slit # Use long and thin disconnected dart for a cutout new_edges, _, int_edges = pyg.ops.cut_into_edge( pyg.EdgeSeqFactory.dart_shape(2, depth=slit * length), # a very thin cutout bottom, offset=bottom.length() / 2, right=True) self.edges.substitute(bottom, new_edges) bottom = int_edges if left_slit: frac = left_slit new_left_bottom = left_bottom.subdivide_len([1 - frac, frac]) left.substitute(left_bottom, new_left_bottom[0]) self.edges.substitute(left_bottom, new_left_bottom) left_bottom = new_left_bottom[0] if right_slit: frac = right_slit new_rbottom = right_bottom.subdivide_len([frac, 1 - frac]) right.substitute(right_bottom, new_rbottom[1]) self.edges.substitute(right_bottom, new_rbottom) right_bottom = new_rbottom[1] if side_cut is not None: try: # Add a stylistic cutout to the skirt new_edges, _, int_edges = pyg.ops.cut_into_edge( side_cut, left_bottom, offset=left_bottom.length() / 2, right=True, flip_target=flip_side_cut) except: # Skip adding the cut if it doesn't fit (e.g. because of the slit) pass else: self.edges.substitute(left_bottom, new_edges) left.substitute(left_bottom, new_edges) # Default placement self.top_center_pivot() self.translation = [-hips / 2, 5, 0] # Out interfaces (easier to define before adding a dart) # Adding ruffle factor on the hip line extention (used in back panel) self.interfaces = { 'bottom': pyg.Interface(self, bottom), 'right': pyg.Interface(self, right, [1] * (len(right) - 1) + [hipline_ext]), 'left': pyg.Interface(self, left, [hipline_ext] + [1] * (len(left) - 1)), } self.interfaces['left'].edges_flipping[0] = True self.interfaces['right'].edges_flipping[-1] = True # Add top darts if w_diff > hw_shift: dart_width = w_diff - hw_shift top_edges, int_edges = self.add_darts(top, dart_width, dart_depth, dart_position, double_dart=double_dart) self.interfaces['top'] = pyg.Interface( self, int_edges, ruffle=int_edges.length() / match_top_int_to if match_top_int_to is not None else 1. ) self.edges.substitute(top, top_edges) else: self.interfaces['top'] = pyg.Interface( self, top, ruffle=top.length() / match_top_int_to if match_top_int_to is not None else 1. ) def add_darts(self, top, dart_width, dart_depth, dart_position, double_dart=False): top_edge_len = top.length() if double_dart: # TODOLOW Avoid hardcoding for matching with the top? dist = dart_position * 0.5 # Dist between darts -> dist between centers offsets_mid = [ - (dart_position + dist / 2 + dart_width / 2) - dart_width / 4, - (dart_position - dist / 2) - dart_width / 4, dart_position - dist / 2 + dart_width / 4, dart_position + dist / 2 + dart_width / 2 + dart_width / 4, ] # dart_shape = pyp.EdgeSeqFactory.dart_shape(dart_width, dart_depth) dart_shape_full = pyg.EdgeSeqFactory.dart_shape(dart_width / 2, dart_depth) dart_shape_small = pyg.EdgeSeqFactory.dart_shape(dart_width / 2, dart_depth * 0.9) darts = [ dart_shape_small, dart_shape_full, dart_shape_full, dart_shape_small, ] else: offsets_mid = [ - dart_position - dart_width / 2, dart_position + dart_width / 2, ] dart_shape = pyg.EdgeSeqFactory.dart_shape(dart_width, dart_depth) darts = [ dart_shape, dart_shape, ] top_edges, int_edges = pyg.EdgeSequence(top), pyg.EdgeSequence(top) for off, dart in zip(offsets_mid, darts): left_edge_len = top_edges[-1].length() top_edges, int_edges = self.add_dart( dart, top_edges[-1], offset=(left_edge_len - top_edge_len / 2) + off, edge_seq=top_edges, int_edge_seq=int_edges ) return top_edges, int_edges # Full garments - Components class PencilSkirt(StackableSkirtComponent): def __init__(self, body, design, tag='', length=None, rise=None, slit=True, **kwargs) -> None: super().__init__(body, design, tag) design = design['pencil-skirt'] self.design = design # Make accessible from outside # condition if design['style_side_cut']['v'] is not None: depth = 0.7 * (body['hips'] / 4 - body['bust_points'] / 2) shape_class = getattr(shapes, design['style_side_cut']['v']) style_shape_l, style_shape_r = shape_class( width=depth * 1.5, depth=depth, n_rays=6, d_rays=depth*0.2, filename=design['style_side_file']['v'] if 'style_side_file' in design else None ) else: style_shape_l, style_shape_r = None, None # Force from arguments if given self.rise = design['rise']['v'] if rise is None else rise waist, hips_depth, back_waist = self.eval_rise(self.rise) if length is None: length = design['length']['v'] * body['_leg_length'] # Depends on leg length else: length = length - hips_depth self.front = FittedSkirtPanel( 'skirt_front', body, design, (waist - back_waist) / 2, (body['hips'] - body['hip_back_width']) / 2, hips_depth=hips_depth, length=length, dart_position=body['bust_points'] / 2, dart_frac=0.8, # Diff for front and back match_top_int_to=(body['waist'] - body['waist_back_width']), slit=design['front_slit']['v'] if slit else 0, left_slit=design['left_slit']['v'] if slit else 0, right_slit=design['right_slit']['v'] if slit else 0, side_cut=style_shape_l ).translate_to([0, body['_waist_level'], 25]) self.back = FittedSkirtPanel( 'skirt_back', body, design, back_waist / 2, body['hip_back_width'] / 2, length=length, hips_depth=hips_depth, hipline_ext=1.05, dart_position=body['bum_points'] / 2, dart_frac=0.85, double_dart=True, match_top_int_to=body['waist_back_width'], slit=design['back_slit']['v'] if slit else 0, left_slit=design['left_slit']['v'] if slit else 0, right_slit=design['right_slit']['v'] if slit else 0, side_cut=style_shape_r, flip_side_cut=False, ).translate_to([0, body['_waist_level'], -20]) self.stitching_rules = pyg.Stitches( (self.front.interfaces['right'], self.back.interfaces['right']), (self.front.interfaces['left'], self.back.interfaces['left']) ) # Reusing interfaces of sub-panels as interfaces of this component self.interfaces = { 'top_f': self.front.interfaces['top'], 'top_b': self.back.interfaces['top'], 'top': pyg.Interface.from_multiple( self.front.interfaces['top'].flip_edges(), self.back.interfaces['top'].reverse(with_edge_dir_reverse=True) ), 'bottom_f': self.front.interfaces['bottom'], 'bottom_b': self.back.interfaces['bottom'], 'bottom': pyg.Interface.from_multiple( self.front.interfaces['bottom'], self.back.interfaces['bottom'] ) } def length(self): return self.front.length() class Skirt2(StackableSkirtComponent): """Simple 2 panel skirt""" def __init__(self, body, design, tag='', length=None, rise=None, slit=True, top_ruffles=True, min_len=5) -> None: super().__init__(body, design, tag) design = design['skirt'] self.rise = design['rise']['v'] if rise is None else rise waist, hip_line, back_waist = self.eval_rise(self.rise) # Force from arguments if given if length is None: length = hip_line + design['length']['v'] * body['_leg_length'] # Depends on leg length # NOTE: with some combinations of rise and length parameters length may become too small/negative # Hence putting a min positive value here length = max(length, min_len) self.front = SkirtPanel( f'skirt_front_{tag}' if tag else 'skirt_front', waist_length=waist - back_waist, length=length, ruffles=design['ruffle']['v'] if top_ruffles else 1, # Only if on waistband flare=design['flare']['v'], bottom_cut=design['bottom_cut']['v'] * design['length']['v'] if slit else 0, match_top_int_to=(body['waist'] - body['waist_back_width']) ).translate_to([0, body['_waist_level'], 25]) self.back = SkirtPanel( f'skirt_back_{tag}' if tag else 'skirt_back', waist_length=back_waist, length=length, ruffles=design['ruffle']['v'] if top_ruffles else 1, # Only if on waistband flare=design['flare']['v'], bottom_cut=design['bottom_cut']['v'] * design['length']['v'] if slit else 0, match_top_int_to=body['waist_back_width'] ).translate_to([0, body['_waist_level'], -20]) self.stitching_rules = pyg.Stitches( (self.front.interfaces['right'], self.back.interfaces['right']), (self.front.interfaces['left'], self.back.interfaces['left']) ) # Reusing interfaces of sub-panels as interfaces of this component self.interfaces = { 'top_f': self.front.interfaces['top'], 'top_b': self.back.interfaces['top'], 'top': pyg.Interface.from_multiple( self.front.interfaces['top'], self.back.interfaces['top'] ), 'bottom_f': self.front.interfaces['bottom'], 'bottom_b': self.back.interfaces['bottom'], 'bottom': pyg.Interface.from_multiple( self.front.interfaces['bottom'], self.back.interfaces['bottom'] ) } def length(self): return self.front.length() class SkirtManyPanels(BaseBottoms): """Round Skirt with many panels""" def __init__(self, body, design, tag='', rise=None, min_len=5) -> None: tag_extra = str(design['flare-skirt']['skirt-many-panels']['n_panels']['v']) tag = f'{tag}_{tag_extra}' if tag else tag_extra super().__init__(body, design, tag=tag, rise=rise) design = design['flare-skirt'] self.rise = design['rise']['v'] if rise is None else rise waist, hip_line, _ = self.eval_rise(self.rise) n_panels = design['skirt-many-panels']['n_panels']['v'] # Length is dependent on length of legs length = hip_line + design['length']['v'] * body['_leg_length'] # NOTE: with some combinations of rise and length parameters, length may become too small/negative # Hence putting a min positive value here length = max(length, min_len) flare_coeff_pi = 1 + design['suns']['v'] * length * 2 * np.pi / waist self.front = ThinSkirtPanel('front', panel_w := waist / n_panels, bottom_width=panel_w * flare_coeff_pi, length=length, b_curvature=design['skirt-many-panels']['panel_curve']['v']) # Move far enough s.t. the widest part of the panels fit on the circle dist = self.front.interfaces['bottom'].edges.length() / (2 * np.tan(np.pi / n_panels)) self.front.translate_to([-dist, body['_waist_level'], 0]) # Align orientation with a body self.front.rotate_by(R.from_euler('XYZ', [0, -90, 0], degrees=True)) self.front.rotate_align([-dist, 0, panel_w / 2]) # Upd interface orientation self.front.interfaces['top'].reverse(True) # Create new panels self.subs = pyg.ops.distribute_Y(self.front, n_panels, name_tag='skirt_panel') # Stitch new components for i in range(1, n_panels): self.stitching_rules.append((self.subs[i - 1].interfaces['left'], self.subs[i].interfaces['right'])) self.stitching_rules.append((self.subs[-1].interfaces['left'], self.subs[0].interfaces['right'])) # Define the interface self.interfaces = { 'top': pyg.Interface.from_multiple(*[sub.interfaces['top'] for sub in self.subs]) } def length(self): return self.front.length()