from copy import deepcopy import numpy as np from scipy.spatial.transform import Rotation as R from assets.garment_programs import bands import pygarment as pyg # ------ Armhole shapes ------ def ArmholeSquare(incl, width, angle, invert=True, **kwargs): """Simple square armhole cut-out Not recommended to use for sleeves, stitching in 3D might be hard if angle is provided, it also calculated the shape of the sleeve interface to attach returns edge sequence and part to be preserved inverted """ edges = pyg.EdgeSeqFactory.from_verts([0, 0], [incl, 0], [incl, width]) if not invert: return edges, None sina, cosa = np.sin(angle), np.cos(angle) l = edges[0].length() sleeve_edges = pyg.EdgeSeqFactory.from_verts( [incl + l*sina, - l*cosa], [incl, 0], [incl, width]) # TODOLOW Bend instead of rotating to avoid sharp connection sleeve_edges.rotate(angle=-angle) return edges, sleeve_edges def ArmholeAngle(incl, width, angle, incl_coeff=0.2, w_coeff=0.2, invert=True, **kwargs): """Piece-wise smooth armhole shape""" diff_incl = incl * (1 - incl_coeff) edges = pyg.EdgeSeqFactory.from_verts( [0, 0], [diff_incl, w_coeff * width], [incl, width]) if not invert: return edges, None sina, cosa = np.sin(angle), np.cos(angle) l = edges[0].length() sleeve_edges = pyg.EdgeSeqFactory.from_verts( [diff_incl + l*sina, w_coeff * width - l*cosa], [diff_incl, w_coeff * width], [incl, width]) # TODOLOW Bend instead of rotating to avoid sharp connection sleeve_edges.rotate(angle=-angle) return edges, sleeve_edges def ArmholeCurve(incl, width, angle, bottom_angle_mix=0, invert=True, verbose=False, **kwargs): """ Classic sleeve opening on Cubic Bezier curves """ # Curvature as parameters? cps = [[0.5, 0.2], [0.8, 0.35]] edge = pyg.CurveEdge([incl, width], [0, 0], cps) edge_as_seq = pyg.EdgeSequence(edge.reverse()) if not invert: return edge_as_seq, None # Initialize inverse (initial guess) # Agle == 0 down_direction = np.array([0, -1]) # Full opening is vertically aligned inv_cps = deepcopy(cps) inv_cps[-1][1] *= -1 # Invert the last inv_edge = pyg.CurveEdge( start=[incl, width], end=(np.array([incl, width]) + down_direction * edge._straight_len()).tolist(), control_points=inv_cps ) # Rotate by desired angle (usually desired sleeve rest angle) inv_edge.rotate(angle=-angle) # Optimize the inverse shape to be nice shortcut = inv_edge.shortcut() rotated_direction = shortcut[-1] - shortcut[0] rotated_direction /= np.linalg.norm(rotated_direction) left_direction = np.array([-1, 0]) mix_factor = bottom_angle_mix dir = (1 - mix_factor) * rotated_direction + ( mix_factor * down_direction if mix_factor > 0 else (- mix_factor * left_direction)) # TODOLOW Remember relative curvature results and reuse them? (speed) fin_inv_edge = pyg.ops.curve_match_tangents( inv_edge.as_curve(), down_direction, # Full opening is vertically aligned dir, target_len=edge.length(), return_as_edge=True, verbose=verbose ) return edge_as_seq, pyg.EdgeSequence(fin_inv_edge.reverse()) # -------- New sleeve definitions ------- class SleevePanel(pyg.Panel): """Trying proper sleeve panel""" def __init__(self, name, body, design, open_shape, length_shift=0, _standing_margin=5): """Define a standard sleeve panel (half a sleeve) * length_shift -- force upd sleeve length by this amount. Can be used to adjust length evaluation to fit the cuff """ super().__init__(name) MIN_LENGTH = 5 # Minimum sleeve length shoulder_angle = np.deg2rad(body['_shoulder_incl']) rest_angle = max(np.deg2rad(design['sleeve_angle']['v']), shoulder_angle) standing = design['standing_shoulder']['v'] # Calculating extension size & end size before applying ruffles # Since ruffles add to pattern length & width, but not to de-facto # sleeve length in 3D end_width = design['end_width']['v'] * abs(open_shape[0].start[1] - open_shape[-1].end[1]) # Ensure it fits regardless of parameters end_width = max(end_width, body['wrist'] / 2) # Ruffles at opening if not pyg.utils.close_enough(design['connect_ruffle']['v'], 1): open_shape.extend(design['connect_ruffle']['v']) # -- Main body of a sleeve -- opening_length = abs(open_shape[0].start[0] - open_shape[-1].end[0]) arm_width = abs(open_shape[0].start[1] - open_shape[-1].end[1]) # Length from the border of the opening to the end of the sleeve length = design['length']['v'] * (body['arm_length'] - opening_length) # NOTE: Asked to reduce by too much: reduce as much as possible length = max(length + length_shift, MIN_LENGTH) self.edges = pyg.EdgeSeqFactory.from_verts( [0, 0], [0, -end_width], [length, -arm_width] ) # Align the opening open_shape.snap_to(self.edges[-1].end) open_shape[0].start = self.edges[-1].end # chain self.edges.append(open_shape) # Fin self.edges.close_loop() if standing: if rest_angle > (shoulder_angle + np.deg2rad(_standing_margin)): # Add a "shelve" to create square shoulder appearance top_edge = self.edges[-1] start = top_edge.start len = design['standing_shoulder_len']['v'] x_shift = len * np.cos(rest_angle - shoulder_angle) y_shift = len * np.sin(rest_angle - shoulder_angle) standing_edge = pyg.Edge( start=start, end=[start[0] - x_shift, start[1] + y_shift] ) top_edge.start = standing_edge.end self.edges.substitute(top_edge, [standing_edge, top_edge]) else: if self.verbose: print(f'{self.__class__.__name__}::WARNING::' f'Sleeve rest angle {np.rad2deg(rest_angle):.3f} should be ' f'larger than shoulder angle {body["_shoulder_incl"]} by ' f'at least {_standing_margin} deg to enable ' 'standing shoulder. Standing shoulder ignored') standing = False # Interfaces self.interfaces = { # NOTE: interface needs reversing because the open_shape was reversed for construction 'in': pyg.Interface(self, open_shape, ruffle=design['connect_ruffle']['v']), 'out': pyg.Interface(self, self.edges[0], ruffle=design['cuff']['top_ruffle']['v']), 'top': pyg.Interface(self, self.edges[-2:] if standing else self.edges[-1]), 'bottom': pyg.Interface(self, self.edges[1]) } # Default placement self.set_pivot(self.edges[-1].start) self.translate_to([ - body['shoulder_w'] / 2, body['height'] - body['head_l'], 0, ]) def length(self, longest_dim=False): return self.interfaces['bottom'].edges.length() class Sleeve(pyg.Component): """Trying to do a proper sleeve""" def __init__(self, tag, body, design, front_w, back_w): """Defintion of a sleeve: * front_w, back_w: the width front and the back of the top the sleeve will attach to -- needed for correct share calculations They may be * Specified as scalar numbers * Specified as functions w.r.t. the requested vertical level (=> calculated width of a horizontal slice) """ super().__init__(f'{self.__class__.__name__}_{tag}') design = design['sleeve'] self.design = design self.body = body sleeve_balance = body['_base_sleeve_balance'] / 2 rest_angle = max(np.deg2rad(design['sleeve_angle']['v']), np.deg2rad(body['_shoulder_incl'])) connecting_width = design['connecting_width']['v'] smoothing_coeff = design['smoothing_coeff']['v'] front_w = front_w(connecting_width) if callable(front_w) else front_w back_w = back_w(connecting_width) if callable(back_w) else back_w # --- Define sleeve opening shapes ---- # NOTE: Non-trad armholes only for sleeveless styles due to # unclear inversion and stitching errors (see below) armhole = globals()[design['armhole_shape']['v']] if design['sleeveless']['v'] else ArmholeCurve front_project, front_opening = armhole( front_w - sleeve_balance, connecting_width, angle=rest_angle, incl_coeff=smoothing_coeff, w_coeff=smoothing_coeff, invert=not design['sleeveless']['v'], bottom_angle_mix=design['opening_dir_mix']['v'], verbose=self.verbose ) back_project, back_opening = armhole( back_w - sleeve_balance, connecting_width, angle=rest_angle, incl_coeff=smoothing_coeff, w_coeff=smoothing_coeff, invert=not design['sleeveless']['v'], bottom_angle_mix=design['opening_dir_mix']['v'] ) self.interfaces = { 'in_front_shape': pyg.Interface(self, front_project), 'in_back_shape': pyg.Interface(self, back_project) } if design['sleeveless']['v']: # The rest is not needed! return if front_w != back_w: front_opening, back_opening = pyg.ops.even_armhole_openings( front_opening, back_opening, tol=0.2 / front_opening.length(), # ~2mm tolerance as a fraction of length verbose=self.verbose ) # --- Eval length adjustment for cuffs (if any) ---- cuff_len_adj = self._cuff_len_adj() # # ----- Get sleeve panels ------- self.f_sleeve = SleevePanel( f'{tag}_sleeve_f', body, design, front_opening, length_shift=-cuff_len_adj ).translate_by([5, 0, 15]) # self.f_sleeve = SleevePanel( # f'{tag}_sleeve_f', body, design, front_opening, # length_shift=-cuff_len_adj # ) self.b_sleeve = SleevePanel( f'{tag}_sleeve_b', body, design, back_opening, length_shift=-cuff_len_adj ).translate_by([5, 0, -15]) # self.b_sleeve = SleevePanel( # f'{tag}_sleeve_b', body, design, back_opening, # length_shift=-cuff_len_adj # ) # Connect panels self.stitching_rules = pyg.Stitches( (self.f_sleeve.interfaces['top'], self.b_sleeve.interfaces['top']), (self.f_sleeve.interfaces['bottom'], self.b_sleeve.interfaces['bottom']), ) # Interfaces self.interfaces.update({ 'in': pyg.Interface.from_multiple( self.f_sleeve.interfaces['in'], self.b_sleeve.interfaces['in'].reverse(with_edge_dir_reverse=True) ), 'out': pyg.Interface.from_multiple( self.f_sleeve.interfaces['out'], self.b_sleeve.interfaces['out'] ), }) # Cuff if design['cuff']['type']['v']: # Class # Copy to avoid editing original design dict cdesign = deepcopy(design) cuff_circ = self.interfaces['out'].edges.length() / design['cuff']['top_ruffle']['v'] # Ensure it fits regardless of parameters cuff_circ = max(cuff_circ, body['wrist']) cdesign['cuff']['b_width'] = dict(v=cuff_circ) cdesign['cuff']['cuff_len']['v'] = cuff_len_adj cuff_class = getattr(bands, cdesign['cuff']['type']['v']) self.cuff = cuff_class(f'sl_{tag}', cdesign) # Position self.cuff.rotate_by( R.from_euler( 'XYZ', [0, 0, -90], # from -Ox direction degrees=True ) ) self.cuff.place_by_interface( self.cuff.interfaces['top'], self.interfaces['out'], gap=2, alignment='top' ) self.stitching_rules.append( ( self.cuff.interfaces['top'], self.interfaces['out'] ) ) # UPD out interface! self.interfaces['out'] = self.cuff.interfaces['bottom'] # Final rotation of sleeve piece # print('*** arm_pose_angle', body['arm_pose_angle']) self.rotate_by(R.from_euler( 'XYZ', [0, 0, body['arm_pose_angle']], degrees=True)) # Set label self.set_panel_label('arm') def _cuff_len_adj(self): """Eval sleeve length adjustment due to cuffs (if any)""" if not self.design['cuff']['type']['v']: return 0 cuff_len_adj = self.design['cuff']['cuff_len']['v'] * self.body['arm_length'] max_len = self.design['length']['v'] * self.body['arm_length'] if cuff_len_adj > max_len * 0.7: cuff_len_adj = max_len * 0.7 return cuff_len_adj def length(self): if self.design['sleeveless']['v']: return 0 if self.design['cuff']['type']['v']: return self.f_sleeve.length() + self.cuff.length() return self.f_sleeve.length()