Spaces:
Running
Running
| """ | |
| 2D Sewing Pattern Generator from Garment Analysis | |
| Generates flat 2D sewing pattern pieces based on garment parameters. | |
| Each garment type has a parametric pattern model that produces | |
| pattern pieces with proper seam allowances, grain lines, and notches. | |
| """ | |
| import math | |
| import numpy as np | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as mpatches | |
| from matplotlib.patches import FancyArrowPatch | |
| from matplotlib.path import Path as MPath | |
| from io import BytesIO | |
| from PIL import Image | |
| from typing import Dict, List, Tuple, Optional | |
| # Color palette for pattern pieces | |
| PIECE_COLORS = [ | |
| '#FFB3BA', '#BAFFC9', '#BAE1FF', '#FFFFBA', '#E8BAFF', | |
| '#FFD4BA', '#BAF0FF', '#FFBAE8', '#D4FFBA', '#BAC8FF' | |
| ] | |
| def generate_bodice_front(params: Dict) -> Dict: | |
| """Generate front bodice pattern piece.""" | |
| bust = params.get('bust', 90) / 4 + 2 | |
| waist = params.get('waist', 70) / 4 + 2 | |
| shoulder = params.get('shoulder_width', 40) / 2 | |
| bodice_length = params.get('bodice_length', 42) | |
| neckline_depth = params.get('neckline_depth', 8) | |
| neckline_width = params.get('neckline_width', 7) | |
| points = [ | |
| (0, 0), | |
| (waist, 0), | |
| (bust, bodice_length * 0.4), | |
| (bust + 1, bodice_length * 0.7), | |
| (shoulder, bodice_length), | |
| (neckline_width, bodice_length - neckline_depth * 0.3), | |
| (0, bodice_length - neckline_depth), | |
| ] | |
| return { | |
| 'name': 'Front Bodice', | |
| 'points': points, | |
| 'grain_line': ((waist/2, 5), (waist/2, bodice_length - 5)), | |
| 'notches': [(bust, bodice_length * 0.4), (neckline_width, bodice_length - neckline_depth * 0.3)], | |
| 'labels': {'CF': (0, bodice_length/2), 'Bust Line': (bust/2, bodice_length * 0.4)}, | |
| 'fold_line': [(0, 0), (0, bodice_length - neckline_depth)], | |
| } | |
| def generate_bodice_back(params: Dict) -> Dict: | |
| """Generate back bodice pattern piece.""" | |
| bust = params.get('bust', 90) / 4 + 1 | |
| waist = params.get('waist', 70) / 4 + 1 | |
| shoulder = params.get('shoulder_width', 40) / 2 | |
| bodice_length = params.get('bodice_length', 42) | |
| back_neck_depth = params.get('back_neck_depth', 2) | |
| neckline_width = params.get('neckline_width', 7) | |
| points = [ | |
| (0, 0), | |
| (waist, 0), | |
| (bust, bodice_length * 0.4), | |
| (bust + 1, bodice_length * 0.7), | |
| (shoulder, bodice_length), | |
| (neckline_width, bodice_length - back_neck_depth * 0.3), | |
| (0, bodice_length - back_neck_depth), | |
| ] | |
| return { | |
| 'name': 'Back Bodice', | |
| 'points': points, | |
| 'grain_line': ((waist/2, 5), (waist/2, bodice_length - 5)), | |
| 'notches': [(bust, bodice_length * 0.4)], | |
| 'labels': {'CB': (0, bodice_length/2)}, | |
| 'fold_line': [(0, 0), (0, bodice_length - back_neck_depth)], | |
| } | |
| def generate_sleeve(params: Dict) -> Dict: | |
| """Generate sleeve pattern piece.""" | |
| sleeve_length = params.get('sleeve_length', 60) | |
| bicep = params.get('bicep', 30) + 4 | |
| wrist = params.get('wrist', 18) + 2 | |
| cap_height = params.get('cap_height', 14) | |
| half_bicep = bicep / 2 | |
| half_wrist = wrist / 2 | |
| points = [ | |
| (-half_wrist, 0), | |
| (-half_bicep, sleeve_length - cap_height), | |
| (-half_bicep * 0.7, sleeve_length - cap_height * 0.3), | |
| (0, sleeve_length), | |
| (half_bicep * 0.7, sleeve_length - cap_height * 0.3), | |
| (half_bicep, sleeve_length - cap_height), | |
| (half_wrist, 0), | |
| ] | |
| return { | |
| 'name': 'Sleeve', | |
| 'points': points, | |
| 'grain_line': ((0, 5), (0, sleeve_length - 5)), | |
| 'notches': [(0, sleeve_length), (-half_bicep, sleeve_length - cap_height), (half_bicep, sleeve_length - cap_height)], | |
| 'labels': {'Grain': (2, sleeve_length/2)}, | |
| 'quantity': 2, | |
| } | |
| def generate_skirt_front(params: Dict) -> Dict: | |
| """Generate front skirt panel.""" | |
| waist = params.get('waist', 70) / 4 + 1 | |
| hip = params.get('hip', 95) / 4 + 1 | |
| skirt_length = params.get('skirt_length', 55) | |
| hip_depth = 20 | |
| flare = params.get('flare', 0) | |
| hem_width = hip + flare | |
| points = [ | |
| (0, 0), | |
| (waist, 0), | |
| (hip, hip_depth), | |
| (hem_width, skirt_length), | |
| (0, skirt_length), | |
| ] | |
| return { | |
| 'name': 'Front Skirt', | |
| 'points': points, | |
| 'grain_line': ((waist/2, 5), (waist/2, skirt_length - 5)), | |
| 'notches': [(hip, hip_depth)], | |
| 'labels': {'CF': (0, skirt_length/2), 'Hip': (hip/2, hip_depth)}, | |
| 'fold_line': [(0, 0), (0, skirt_length)], | |
| } | |
| def generate_skirt_back(params: Dict) -> Dict: | |
| """Generate back skirt panel.""" | |
| waist = params.get('waist', 70) / 4 + 2 | |
| hip = params.get('hip', 95) / 4 + 2 | |
| skirt_length = params.get('skirt_length', 55) | |
| hip_depth = 20 | |
| flare = params.get('flare', 0) | |
| hem_width = hip + flare | |
| points = [ | |
| (0, 0), | |
| (waist, 0), | |
| (hip, hip_depth), | |
| (hem_width, skirt_length), | |
| (0, skirt_length), | |
| ] | |
| return { | |
| 'name': 'Back Skirt', | |
| 'points': points, | |
| 'grain_line': ((waist/2, 5), (waist/2, skirt_length - 5)), | |
| 'notches': [(hip, hip_depth)], | |
| 'labels': {'CB': (0, skirt_length/2)}, | |
| 'fold_line': [(0, 0), (0, skirt_length)], | |
| } | |
| def generate_collar(params: Dict) -> Dict: | |
| """Generate collar piece.""" | |
| collar_type = params.get('collar_type', 'standard') | |
| collar_height = params.get('collar_height', 5) | |
| neck_circumference = params.get('neck_circumference', 38) / 2 + 1 | |
| if collar_type == 'mandarin': | |
| points = [ | |
| (0, 0), (neck_circumference, 0), | |
| (neck_circumference, collar_height), (0, collar_height), | |
| ] | |
| elif collar_type == 'peter_pan': | |
| points = [ | |
| (0, 0), (neck_circumference, 0), | |
| (neck_circumference + 3, collar_height * 0.5), | |
| (neck_circumference + 5, collar_height), | |
| (neck_circumference * 0.7, collar_height + 3), | |
| (0, collar_height + 2), | |
| ] | |
| else: | |
| points = [ | |
| (0, 0), (neck_circumference, 0), | |
| (neck_circumference + 2, collar_height * 0.7), | |
| (neck_circumference - 1, collar_height), (0, collar_height), | |
| ] | |
| return { | |
| 'name': f'Collar ({collar_type.title()})', | |
| 'points': points, | |
| 'grain_line': ((neck_circumference/2, 1), (neck_circumference/2, collar_height - 1)), | |
| 'notches': [(0, 0), (neck_circumference, 0)], | |
| 'labels': {'Neck Edge': (neck_circumference/2, -1.5)}, | |
| 'quantity': 2, | |
| } | |
| def generate_pant_front(params: Dict) -> Dict: | |
| """Generate front pant leg piece.""" | |
| waist = params.get('waist', 70) / 4 + 1 | |
| hip = params.get('hip', 95) / 4 + 1 | |
| thigh = params.get('thigh', 55) / 2 + 2 | |
| knee = params.get('knee', 38) / 2 + 1 | |
| ankle = params.get('ankle', 24) / 2 + 1 | |
| pant_length = params.get('pant_length', 100) | |
| crotch_depth = params.get('crotch_depth', 27) | |
| half_thigh = thigh / 2 | |
| crotch_ext = hip * 0.25 | |
| points = [ | |
| (0, 0), (waist, 0), | |
| (hip, crotch_depth * 0.6), | |
| (half_thigh + 2, crotch_depth), | |
| (knee/2 + 2, pant_length * 0.55), | |
| (ankle/2 + 1, pant_length), | |
| (-ankle/2 + 3, pant_length), | |
| (-knee/2 + 5, pant_length * 0.55), | |
| (-crotch_ext + 2, crotch_depth), | |
| (0, crotch_depth * 0.3), | |
| ] | |
| return { | |
| 'name': 'Front Pant', | |
| 'points': points, | |
| 'grain_line': ((waist/3, 5), (waist/3, pant_length - 5)), | |
| 'notches': [(hip, crotch_depth * 0.6), (0, 0)], | |
| 'labels': {'CF': (2, pant_length/2), 'Knee': (0, pant_length * 0.55)}, | |
| 'quantity': 2, | |
| } | |
| def generate_pant_back(params: Dict) -> Dict: | |
| """Generate back pant leg piece.""" | |
| waist = params.get('waist', 70) / 4 + 2 | |
| hip = params.get('hip', 95) / 4 + 2 | |
| thigh = params.get('thigh', 55) / 2 + 3 | |
| knee = params.get('knee', 38) / 2 + 2 | |
| ankle = params.get('ankle', 24) / 2 + 2 | |
| pant_length = params.get('pant_length', 100) | |
| crotch_depth = params.get('crotch_depth', 27) | |
| half_thigh = thigh / 2 | |
| crotch_ext = hip * 0.35 | |
| points = [ | |
| (-2, 1), (waist + 1, 1), | |
| (hip + 1, crotch_depth * 0.6), | |
| (half_thigh + 3, crotch_depth), | |
| (knee/2 + 3, pant_length * 0.55), | |
| (ankle/2 + 2, pant_length), | |
| (-ankle/2 + 2, pant_length), | |
| (-knee/2 + 4, pant_length * 0.55), | |
| (-crotch_ext, crotch_depth), | |
| (-2, crotch_depth * 0.4), | |
| ] | |
| return { | |
| 'name': 'Back Pant', | |
| 'points': points, | |
| 'grain_line': ((waist/3, 5), (waist/3, pant_length - 5)), | |
| 'notches': [(hip + 1, crotch_depth * 0.6)], | |
| 'labels': {'CB': (0, pant_length/2)}, | |
| 'quantity': 2, | |
| } | |
| def generate_waistband(params: Dict) -> Dict: | |
| """Generate waistband piece.""" | |
| waist = params.get('waist', 70) / 2 + 3 | |
| band_height = params.get('waistband_height', 4) | |
| points = [(0, 0), (waist, 0), (waist, band_height), (0, band_height)] | |
| return { | |
| 'name': 'Waistband', | |
| 'points': points, | |
| 'grain_line': ((waist/2, 1), (waist/2, band_height - 1)), | |
| 'notches': [(0, 0), (waist/2, 0), (waist, 0)], | |
| 'labels': {'Grain': (waist/2, band_height + 1)}, | |
| 'quantity': 1, | |
| } | |
| def generate_hood(params: Dict) -> Dict: | |
| """Generate hood pattern piece.""" | |
| head_circ = params.get('head_circumference', 57) / 2 + 3 | |
| hood_height = head_circ * 0.8 | |
| hood_depth = head_circ * 0.6 | |
| points = [ | |
| (0, 0), (0, hood_height * 0.3), | |
| (-2, hood_height * 0.7), (3, hood_height), | |
| (hood_depth * 0.5, hood_height + 3), | |
| (hood_depth, hood_height - 2), | |
| (hood_depth + 2, hood_height * 0.3), | |
| (hood_depth - 3, 0), | |
| ] | |
| return { | |
| 'name': 'Hood', | |
| 'points': points, | |
| 'grain_line': ((hood_depth/2, 5), (hood_depth/2, hood_height - 5)), | |
| 'notches': [(0, 0), (hood_depth - 3, 0)], | |
| 'labels': {'Face Opening': (-3, hood_height * 0.5)}, | |
| 'quantity': 2, | |
| } | |
| def generate_pocket(params: Dict) -> Dict: | |
| """Generate pocket piece.""" | |
| pocket_width = params.get('pocket_width', 14) | |
| pocket_height = params.get('pocket_height', 16) | |
| pocket_type = params.get('pocket_type', 'patch') | |
| if pocket_type == 'patch': | |
| points = [ | |
| (0, 0), (pocket_width, 0), | |
| (pocket_width, pocket_height), | |
| (pocket_width * 0.8, pocket_height + 2), | |
| (pocket_width * 0.2, pocket_height + 2), | |
| (0, pocket_height), | |
| ] | |
| else: | |
| points = [ | |
| (0, 0), (pocket_width, 0), | |
| (pocket_width, pocket_height * 0.3), | |
| (0, pocket_height * 0.3), | |
| ] | |
| return { | |
| 'name': f'Pocket ({pocket_type.title()})', | |
| 'points': points, | |
| 'grain_line': ((pocket_width/2, 1), (pocket_width/2, pocket_height - 1)), | |
| 'notches': [], | |
| 'labels': {}, | |
| 'quantity': 2, | |
| } | |
| def generate_cuff(params: Dict) -> Dict: | |
| """Generate cuff piece.""" | |
| wrist = params.get('wrist', 18) + 3 | |
| cuff_height = params.get('cuff_height', 6) | |
| points = [(0, 0), (wrist, 0), (wrist, cuff_height), (0, cuff_height)] | |
| return { | |
| 'name': 'Cuff', | |
| 'points': points, | |
| 'grain_line': ((wrist/2, 1), (wrist/2, cuff_height - 1)), | |
| 'notches': [(0, 0), (wrist, 0)], | |
| 'labels': {}, | |
| 'quantity': 2, | |
| } | |
| def get_pattern_pieces(garment_type: str, params: Dict) -> List[Dict]: | |
| """Get all pattern pieces for a garment type.""" | |
| pieces = [] | |
| garment_type = garment_type.lower() | |
| if garment_type in ['shirt', 'blouse', 'top', 't-shirt', 'tee']: | |
| pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params)] | |
| if params.get('has_collar', False): | |
| pieces.append(generate_collar(params)) | |
| if params.get('has_cuffs', False): | |
| pieces.append(generate_cuff(params)) | |
| if params.get('has_pockets', False): | |
| pieces.append(generate_pocket(params)) | |
| elif garment_type in ['dress']: | |
| pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params), | |
| generate_skirt_front(params), generate_skirt_back(params)] | |
| if params.get('has_collar', False): | |
| pieces.append(generate_collar(params)) | |
| elif garment_type in ['skirt']: | |
| pieces = [generate_skirt_front(params), generate_skirt_back(params), generate_waistband(params)] | |
| elif garment_type in ['pants', 'trousers', 'jeans']: | |
| pieces = [generate_pant_front(params), generate_pant_back(params), generate_waistband(params)] | |
| if params.get('has_pockets', False): | |
| pieces.append(generate_pocket(params)) | |
| elif garment_type in ['jacket', 'coat', 'blazer']: | |
| p = {**params, 'bodice_length': params.get('jacket_length', 65)} | |
| pieces = [generate_bodice_front(p), generate_bodice_back(p), generate_sleeve(p)] | |
| if params.get('has_collar', True): | |
| pieces.append(generate_collar(p)) | |
| if params.get('has_pockets', True): | |
| pieces.append(generate_pocket(p)) | |
| if params.get('has_hood', False): | |
| pieces.append(generate_hood(p)) | |
| elif garment_type in ['hoodie', 'sweatshirt']: | |
| p = {**params, 'bodice_length': params.get('jacket_length', 65)} | |
| pieces = [generate_bodice_front(p), generate_bodice_back(p), generate_sleeve(p), generate_hood(p)] | |
| if params.get('has_pockets', True): | |
| p['pocket_type'] = 'patch' | |
| pieces.append(generate_pocket(p)) | |
| elif garment_type in ['vest']: | |
| p = {**params, 'bodice_length': params.get('vest_length', 55)} | |
| pieces = [generate_bodice_front(p), generate_bodice_back(p)] | |
| else: | |
| pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params)] | |
| return pieces | |
| def render_pattern_pieces(pieces: List[Dict], title: str = "2D Sewing Pattern") -> Image.Image: | |
| """Render pattern pieces as a clean technical drawing.""" | |
| n = len(pieces) | |
| if n == 0: | |
| fig, ax = plt.subplots(1, 1, figsize=(10, 8)) | |
| ax.text(0.5, 0.5, 'No pattern pieces generated', ha='center', va='center', fontsize=16) | |
| ax.axis('off') | |
| buf = BytesIO() | |
| fig.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white') | |
| plt.close(fig) | |
| buf.seek(0) | |
| return Image.open(buf) | |
| cols = min(3, n) | |
| rows = math.ceil(n / cols) | |
| fig, axes = plt.subplots(rows, cols, figsize=(7 * cols, 7 * rows)) | |
| fig.suptitle(title, fontsize=18, fontweight='bold', y=0.98) | |
| if rows == 1 and cols == 1: | |
| axes = np.array([axes]) | |
| axes = np.atleast_2d(axes) | |
| for idx in range(rows * cols): | |
| row, col = divmod(idx, cols) | |
| ax = axes[row, col] | |
| if idx >= n: | |
| ax.axis('off') | |
| continue | |
| piece = pieces[idx] | |
| points = piece['points'] | |
| color = PIECE_COLORS[idx % len(PIECE_COLORS)] | |
| pts = points + [points[0]] | |
| xs = [p[0] for p in pts] | |
| ys = [p[1] for p in pts] | |
| ax.fill(xs, ys, color=color, alpha=0.3, linewidth=0) | |
| ax.plot(xs, ys, 'k-', linewidth=1.5, label='Cut line') | |
| cx = np.mean([p[0] for p in points]) | |
| cy = np.mean([p[1] for p in points]) | |
| scale = 0.92 | |
| inner_xs = [cx + (x - cx) * scale for x in xs] | |
| inner_ys = [cy + (y - cy) * scale for y in ys] | |
| ax.plot(inner_xs, inner_ys, 'k--', linewidth=0.7, alpha=0.5, label='Seam line') | |
| if 'fold_line' in piece: | |
| fl = piece['fold_line'] | |
| ax.plot([fl[0][0], fl[1][0]], [fl[0][1], fl[1][1]], 'b-.', linewidth=1.2, label='Fold') | |
| if 'grain_line' in piece: | |
| gl = piece['grain_line'] | |
| ax.annotate('', xy=gl[1], xytext=gl[0], | |
| arrowprops=dict(arrowstyle='->', color='red', lw=1.5)) | |
| mid_x = (gl[0][0] + gl[1][0]) / 2 | |
| mid_y = (gl[0][1] + gl[1][1]) / 2 | |
| ax.text(mid_x + 1, mid_y, 'GRAIN', fontsize=7, color='red', rotation=90, | |
| ha='left', va='center', fontweight='bold') | |
| if 'notches' in piece: | |
| for nx, ny in piece['notches']: | |
| ax.plot(nx, ny, 'k^', markersize=6) | |
| qty = piece.get('quantity', 1) | |
| qty_str = f" (×{qty})" if qty > 1 else " (×1 on fold)" if 'fold_line' in piece else "" | |
| ax.set_title(f"{piece['name']}{qty_str}", fontsize=12, fontweight='bold', pad=10) | |
| if 'labels' in piece: | |
| for label, (lx, ly) in piece['labels'].items(): | |
| ax.text(lx, ly, label, fontsize=8, ha='center', va='center', | |
| color='navy', style='italic', | |
| bbox=dict(boxstyle='round,pad=0.2', facecolor='white', alpha=0.7, edgecolor='none')) | |
| ax.set_aspect('equal') | |
| ax.grid(True, alpha=0.2, linestyle=':') | |
| ax.tick_params(labelsize=7) | |
| ax.set_xlabel('cm', fontsize=8) | |
| ax.set_ylabel('cm', fontsize=8) | |
| if n > 0: | |
| from matplotlib.lines import Line2D | |
| legend_elements = [ | |
| Line2D([0], [0], color='k', linewidth=1.5, label='Cut line'), | |
| Line2D([0], [0], color='k', linewidth=0.7, linestyle='--', label='Seam line'), | |
| Line2D([0], [0], color='b', linewidth=1.2, linestyle='-.', label='Fold line'), | |
| Line2D([0], [0], color='red', linewidth=1.5, marker='>', markersize=5, label='Grain line'), | |
| Line2D([0], [0], color='k', linewidth=0, marker='^', markersize=6, label='Notch'), | |
| ] | |
| fig.legend(handles=legend_elements, loc='lower center', ncol=5, fontsize=9, | |
| bbox_to_anchor=(0.5, 0.01), frameon=True, fancybox=True) | |
| plt.tight_layout(rect=[0, 0.04, 1, 0.96]) | |
| buf = BytesIO() | |
| fig.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white') | |
| plt.close(fig) | |
| buf.seek(0) | |
| return Image.open(buf) | |
| def generate_pattern_from_analysis(analysis: Dict) -> Tuple[Image.Image, str]: | |
| """Main entry: generate 2D pattern from garment analysis dict.""" | |
| garment_type = analysis.get('garment_type', 'shirt') | |
| measurements = analysis.get('measurements', {}) | |
| features = analysis.get('features', {}) | |
| params = {**measurements, **features} | |
| pieces = get_pattern_pieces(garment_type, params) | |
| title = f"2D Sewing Pattern — {garment_type.title()}" | |
| pattern_image = render_pattern_pieces(pieces, title) | |
| summary_lines = [f"## Pattern: {garment_type.title()}\n"] | |
| summary_lines.append(f"**Total pieces:** {len(pieces)}\n") | |
| for i, piece in enumerate(pieces): | |
| qty = piece.get('quantity', 1) | |
| fold = " (cut on fold)" if 'fold_line' in piece else "" | |
| summary_lines.append(f"{i+1}. **{piece['name']}** — Cut {qty}{fold}") | |
| summary_lines.append(f"\n### Measurements Used (cm):") | |
| for key, val in params.items(): | |
| if isinstance(val, (int, float)): | |
| summary_lines.append(f"- {key.replace('_', ' ').title()}: {val}") | |
| summary_lines.append(f"\n*Pattern includes 1cm seam allowance (dashed inner line).*") | |
| summary_lines.append(f"*Arrows indicate grain line direction.*") | |
| return pattern_image, "\n".join(summary_lines) | |