Files
2025-07-03 17:03:00 +08:00

374 lines
14 KiB
Python

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()