vikashmakeit commited on
Commit
d0a37b9
·
verified ·
1 Parent(s): 0936571

Add pattern generator module

Browse files
Files changed (1) hide show
  1. pattern_generator.py +545 -0
pattern_generator.py ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 2D Sewing Pattern Generator from Garment Analysis
3
+
4
+ Generates flat 2D sewing pattern pieces based on garment parameters.
5
+ Each garment type has a parametric pattern model that produces
6
+ pattern pieces with proper seam allowances, grain lines, and notches.
7
+ """
8
+
9
+ import math
10
+ import numpy as np
11
+ import matplotlib
12
+ matplotlib.use('Agg')
13
+ import matplotlib.pyplot as plt
14
+ import matplotlib.patches as mpatches
15
+ from matplotlib.patches import FancyArrowPatch
16
+ from matplotlib.path import Path as MPath
17
+ from io import BytesIO
18
+ from PIL import Image
19
+ from typing import Dict, List, Tuple, Optional
20
+
21
+ # Color palette for pattern pieces
22
+ PIECE_COLORS = [
23
+ '#FFB3BA', '#BAFFC9', '#BAE1FF', '#FFFFBA', '#E8BAFF',
24
+ '#FFD4BA', '#BAF0FF', '#FFBAE8', '#D4FFBA', '#BAC8FF'
25
+ ]
26
+
27
+
28
+ def generate_bodice_front(params: Dict) -> Dict:
29
+ """Generate front bodice pattern piece."""
30
+ bust = params.get('bust', 90) / 4 + 2
31
+ waist = params.get('waist', 70) / 4 + 2
32
+ shoulder = params.get('shoulder_width', 40) / 2
33
+ bodice_length = params.get('bodice_length', 42)
34
+ neckline_depth = params.get('neckline_depth', 8)
35
+ neckline_width = params.get('neckline_width', 7)
36
+
37
+ points = [
38
+ (0, 0),
39
+ (waist, 0),
40
+ (bust, bodice_length * 0.4),
41
+ (bust + 1, bodice_length * 0.7),
42
+ (shoulder, bodice_length),
43
+ (neckline_width, bodice_length - neckline_depth * 0.3),
44
+ (0, bodice_length - neckline_depth),
45
+ ]
46
+
47
+ return {
48
+ 'name': 'Front Bodice',
49
+ 'points': points,
50
+ 'grain_line': ((waist/2, 5), (waist/2, bodice_length - 5)),
51
+ 'notches': [(bust, bodice_length * 0.4), (neckline_width, bodice_length - neckline_depth * 0.3)],
52
+ 'labels': {'CF': (0, bodice_length/2), 'Bust Line': (bust/2, bodice_length * 0.4)},
53
+ 'fold_line': [(0, 0), (0, bodice_length - neckline_depth)],
54
+ }
55
+
56
+
57
+ def generate_bodice_back(params: Dict) -> Dict:
58
+ """Generate back bodice pattern piece."""
59
+ bust = params.get('bust', 90) / 4 + 1
60
+ waist = params.get('waist', 70) / 4 + 1
61
+ shoulder = params.get('shoulder_width', 40) / 2
62
+ bodice_length = params.get('bodice_length', 42)
63
+ back_neck_depth = params.get('back_neck_depth', 2)
64
+ neckline_width = params.get('neckline_width', 7)
65
+
66
+ points = [
67
+ (0, 0),
68
+ (waist, 0),
69
+ (bust, bodice_length * 0.4),
70
+ (bust + 1, bodice_length * 0.7),
71
+ (shoulder, bodice_length),
72
+ (neckline_width, bodice_length - back_neck_depth * 0.3),
73
+ (0, bodice_length - back_neck_depth),
74
+ ]
75
+
76
+ return {
77
+ 'name': 'Back Bodice',
78
+ 'points': points,
79
+ 'grain_line': ((waist/2, 5), (waist/2, bodice_length - 5)),
80
+ 'notches': [(bust, bodice_length * 0.4)],
81
+ 'labels': {'CB': (0, bodice_length/2)},
82
+ 'fold_line': [(0, 0), (0, bodice_length - back_neck_depth)],
83
+ }
84
+
85
+
86
+ def generate_sleeve(params: Dict) -> Dict:
87
+ """Generate sleeve pattern piece."""
88
+ sleeve_length = params.get('sleeve_length', 60)
89
+ bicep = params.get('bicep', 30) + 4
90
+ wrist = params.get('wrist', 18) + 2
91
+ cap_height = params.get('cap_height', 14)
92
+
93
+ half_bicep = bicep / 2
94
+ half_wrist = wrist / 2
95
+
96
+ points = [
97
+ (-half_wrist, 0),
98
+ (-half_bicep, sleeve_length - cap_height),
99
+ (-half_bicep * 0.7, sleeve_length - cap_height * 0.3),
100
+ (0, sleeve_length),
101
+ (half_bicep * 0.7, sleeve_length - cap_height * 0.3),
102
+ (half_bicep, sleeve_length - cap_height),
103
+ (half_wrist, 0),
104
+ ]
105
+
106
+ return {
107
+ 'name': 'Sleeve',
108
+ 'points': points,
109
+ 'grain_line': ((0, 5), (0, sleeve_length - 5)),
110
+ 'notches': [(0, sleeve_length), (-half_bicep, sleeve_length - cap_height), (half_bicep, sleeve_length - cap_height)],
111
+ 'labels': {'Grain': (2, sleeve_length/2)},
112
+ 'quantity': 2,
113
+ }
114
+
115
+
116
+ def generate_skirt_front(params: Dict) -> Dict:
117
+ """Generate front skirt panel."""
118
+ waist = params.get('waist', 70) / 4 + 1
119
+ hip = params.get('hip', 95) / 4 + 1
120
+ skirt_length = params.get('skirt_length', 55)
121
+ hip_depth = 20
122
+ flare = params.get('flare', 0)
123
+ hem_width = hip + flare
124
+
125
+ points = [
126
+ (0, 0),
127
+ (waist, 0),
128
+ (hip, hip_depth),
129
+ (hem_width, skirt_length),
130
+ (0, skirt_length),
131
+ ]
132
+
133
+ return {
134
+ 'name': 'Front Skirt',
135
+ 'points': points,
136
+ 'grain_line': ((waist/2, 5), (waist/2, skirt_length - 5)),
137
+ 'notches': [(hip, hip_depth)],
138
+ 'labels': {'CF': (0, skirt_length/2), 'Hip': (hip/2, hip_depth)},
139
+ 'fold_line': [(0, 0), (0, skirt_length)],
140
+ }
141
+
142
+
143
+ def generate_skirt_back(params: Dict) -> Dict:
144
+ """Generate back skirt panel."""
145
+ waist = params.get('waist', 70) / 4 + 2
146
+ hip = params.get('hip', 95) / 4 + 2
147
+ skirt_length = params.get('skirt_length', 55)
148
+ hip_depth = 20
149
+ flare = params.get('flare', 0)
150
+ hem_width = hip + flare
151
+
152
+ points = [
153
+ (0, 0),
154
+ (waist, 0),
155
+ (hip, hip_depth),
156
+ (hem_width, skirt_length),
157
+ (0, skirt_length),
158
+ ]
159
+
160
+ return {
161
+ 'name': 'Back Skirt',
162
+ 'points': points,
163
+ 'grain_line': ((waist/2, 5), (waist/2, skirt_length - 5)),
164
+ 'notches': [(hip, hip_depth)],
165
+ 'labels': {'CB': (0, skirt_length/2)},
166
+ 'fold_line': [(0, 0), (0, skirt_length)],
167
+ }
168
+
169
+
170
+ def generate_collar(params: Dict) -> Dict:
171
+ """Generate collar piece."""
172
+ collar_type = params.get('collar_type', 'standard')
173
+ collar_height = params.get('collar_height', 5)
174
+ neck_circumference = params.get('neck_circumference', 38) / 2 + 1
175
+
176
+ if collar_type == 'mandarin':
177
+ points = [
178
+ (0, 0), (neck_circumference, 0),
179
+ (neck_circumference, collar_height), (0, collar_height),
180
+ ]
181
+ elif collar_type == 'peter_pan':
182
+ points = [
183
+ (0, 0), (neck_circumference, 0),
184
+ (neck_circumference + 3, collar_height * 0.5),
185
+ (neck_circumference + 5, collar_height),
186
+ (neck_circumference * 0.7, collar_height + 3),
187
+ (0, collar_height + 2),
188
+ ]
189
+ else:
190
+ points = [
191
+ (0, 0), (neck_circumference, 0),
192
+ (neck_circumference + 2, collar_height * 0.7),
193
+ (neck_circumference - 1, collar_height), (0, collar_height),
194
+ ]
195
+
196
+ return {
197
+ 'name': f'Collar ({collar_type.title()})',
198
+ 'points': points,
199
+ 'grain_line': ((neck_circumference/2, 1), (neck_circumference/2, collar_height - 1)),
200
+ 'notches': [(0, 0), (neck_circumference, 0)],
201
+ 'labels': {'Neck Edge': (neck_circumference/2, -1.5)},
202
+ 'quantity': 2,
203
+ }
204
+
205
+
206
+ def generate_pant_front(params: Dict) -> Dict:
207
+ """Generate front pant leg piece."""
208
+ waist = params.get('waist', 70) / 4 + 1
209
+ hip = params.get('hip', 95) / 4 + 1
210
+ thigh = params.get('thigh', 55) / 2 + 2
211
+ knee = params.get('knee', 38) / 2 + 1
212
+ ankle = params.get('ankle', 24) / 2 + 1
213
+ pant_length = params.get('pant_length', 100)
214
+ crotch_depth = params.get('crotch_depth', 27)
215
+
216
+ half_thigh = thigh / 2
217
+ crotch_ext = hip * 0.25
218
+
219
+ points = [
220
+ (0, 0), (waist, 0),
221
+ (hip, crotch_depth * 0.6),
222
+ (half_thigh + 2, crotch_depth),
223
+ (knee/2 + 2, pant_length * 0.55),
224
+ (ankle/2 + 1, pant_length),
225
+ (-ankle/2 + 3, pant_length),
226
+ (-knee/2 + 5, pant_length * 0.55),
227
+ (-crotch_ext + 2, crotch_depth),
228
+ (0, crotch_depth * 0.3),
229
+ ]
230
+
231
+ return {
232
+ 'name': 'Front Pant',
233
+ 'points': points,
234
+ 'grain_line': ((waist/3, 5), (waist/3, pant_length - 5)),
235
+ 'notches': [(hip, crotch_depth * 0.6), (0, 0)],
236
+ 'labels': {'CF': (2, pant_length/2), 'Knee': (0, pant_length * 0.55)},
237
+ 'quantity': 2,
238
+ }
239
+
240
+
241
+ def generate_pant_back(params: Dict) -> Dict:
242
+ """Generate back pant leg piece."""
243
+ waist = params.get('waist', 70) / 4 + 2
244
+ hip = params.get('hip', 95) / 4 + 2
245
+ thigh = params.get('thigh', 55) / 2 + 3
246
+ knee = params.get('knee', 38) / 2 + 2
247
+ ankle = params.get('ankle', 24) / 2 + 2
248
+ pant_length = params.get('pant_length', 100)
249
+ crotch_depth = params.get('crotch_depth', 27)
250
+
251
+ half_thigh = thigh / 2
252
+ crotch_ext = hip * 0.35
253
+
254
+ points = [
255
+ (-2, 1), (waist + 1, 1),
256
+ (hip + 1, crotch_depth * 0.6),
257
+ (half_thigh + 3, crotch_depth),
258
+ (knee/2 + 3, pant_length * 0.55),
259
+ (ankle/2 + 2, pant_length),
260
+ (-ankle/2 + 2, pant_length),
261
+ (-knee/2 + 4, pant_length * 0.55),
262
+ (-crotch_ext, crotch_depth),
263
+ (-2, crotch_depth * 0.4),
264
+ ]
265
+
266
+ return {
267
+ 'name': 'Back Pant',
268
+ 'points': points,
269
+ 'grain_line': ((waist/3, 5), (waist/3, pant_length - 5)),
270
+ 'notches': [(hip + 1, crotch_depth * 0.6)],
271
+ 'labels': {'CB': (0, pant_length/2)},
272
+ 'quantity': 2,
273
+ }
274
+
275
+
276
+ def generate_waistband(params: Dict) -> Dict:
277
+ """Generate waistband piece."""
278
+ waist = params.get('waist', 70) / 2 + 3
279
+ band_height = params.get('waistband_height', 4)
280
+
281
+ points = [(0, 0), (waist, 0), (waist, band_height), (0, band_height)]
282
+
283
+ return {
284
+ 'name': 'Waistband',
285
+ 'points': points,
286
+ 'grain_line': ((waist/2, 1), (waist/2, band_height - 1)),
287
+ 'notches': [(0, 0), (waist/2, 0), (waist, 0)],
288
+ 'labels': {'Grain': (waist/2, band_height + 1)},
289
+ 'quantity': 1,
290
+ }
291
+
292
+
293
+ def generate_hood(params: Dict) -> Dict:
294
+ """Generate hood pattern piece."""
295
+ head_circ = params.get('head_circumference', 57) / 2 + 3
296
+ hood_height = head_circ * 0.8
297
+ hood_depth = head_circ * 0.6
298
+
299
+ points = [
300
+ (0, 0), (0, hood_height * 0.3),
301
+ (-2, hood_height * 0.7), (3, hood_height),
302
+ (hood_depth * 0.5, hood_height + 3),
303
+ (hood_depth, hood_height - 2),
304
+ (hood_depth + 2, hood_height * 0.3),
305
+ (hood_depth - 3, 0),
306
+ ]
307
+
308
+ return {
309
+ 'name': 'Hood',
310
+ 'points': points,
311
+ 'grain_line': ((hood_depth/2, 5), (hood_depth/2, hood_height - 5)),
312
+ 'notches': [(0, 0), (hood_depth - 3, 0)],
313
+ 'labels': {'Face Opening': (-3, hood_height * 0.5)},
314
+ 'quantity': 2,
315
+ }
316
+
317
+
318
+ def generate_pocket(params: Dict) -> Dict:
319
+ """Generate pocket piece."""
320
+ pocket_width = params.get('pocket_width', 14)
321
+ pocket_height = params.get('pocket_height', 16)
322
+ pocket_type = params.get('pocket_type', 'patch')
323
+
324
+ if pocket_type == 'patch':
325
+ points = [
326
+ (0, 0), (pocket_width, 0),
327
+ (pocket_width, pocket_height),
328
+ (pocket_width * 0.8, pocket_height + 2),
329
+ (pocket_width * 0.2, pocket_height + 2),
330
+ (0, pocket_height),
331
+ ]
332
+ else:
333
+ points = [
334
+ (0, 0), (pocket_width, 0),
335
+ (pocket_width, pocket_height * 0.3),
336
+ (0, pocket_height * 0.3),
337
+ ]
338
+
339
+ return {
340
+ 'name': f'Pocket ({pocket_type.title()})',
341
+ 'points': points,
342
+ 'grain_line': ((pocket_width/2, 1), (pocket_width/2, pocket_height - 1)),
343
+ 'notches': [],
344
+ 'labels': {},
345
+ 'quantity': 2,
346
+ }
347
+
348
+
349
+ def generate_cuff(params: Dict) -> Dict:
350
+ """Generate cuff piece."""
351
+ wrist = params.get('wrist', 18) + 3
352
+ cuff_height = params.get('cuff_height', 6)
353
+
354
+ points = [(0, 0), (wrist, 0), (wrist, cuff_height), (0, cuff_height)]
355
+
356
+ return {
357
+ 'name': 'Cuff',
358
+ 'points': points,
359
+ 'grain_line': ((wrist/2, 1), (wrist/2, cuff_height - 1)),
360
+ 'notches': [(0, 0), (wrist, 0)],
361
+ 'labels': {},
362
+ 'quantity': 2,
363
+ }
364
+
365
+
366
+ def get_pattern_pieces(garment_type: str, params: Dict) -> List[Dict]:
367
+ """Get all pattern pieces for a garment type."""
368
+ pieces = []
369
+ garment_type = garment_type.lower()
370
+
371
+ if garment_type in ['shirt', 'blouse', 'top', 't-shirt', 'tee']:
372
+ pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params)]
373
+ if params.get('has_collar', False):
374
+ pieces.append(generate_collar(params))
375
+ if params.get('has_cuffs', False):
376
+ pieces.append(generate_cuff(params))
377
+ if params.get('has_pockets', False):
378
+ pieces.append(generate_pocket(params))
379
+ elif garment_type in ['dress']:
380
+ pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params),
381
+ generate_skirt_front(params), generate_skirt_back(params)]
382
+ if params.get('has_collar', False):
383
+ pieces.append(generate_collar(params))
384
+ elif garment_type in ['skirt']:
385
+ pieces = [generate_skirt_front(params), generate_skirt_back(params), generate_waistband(params)]
386
+ elif garment_type in ['pants', 'trousers', 'jeans']:
387
+ pieces = [generate_pant_front(params), generate_pant_back(params), generate_waistband(params)]
388
+ if params.get('has_pockets', False):
389
+ pieces.append(generate_pocket(params))
390
+ elif garment_type in ['jacket', 'coat', 'blazer']:
391
+ p = {**params, 'bodice_length': params.get('jacket_length', 65)}
392
+ pieces = [generate_bodice_front(p), generate_bodice_back(p), generate_sleeve(p)]
393
+ if params.get('has_collar', True):
394
+ pieces.append(generate_collar(p))
395
+ if params.get('has_pockets', True):
396
+ pieces.append(generate_pocket(p))
397
+ if params.get('has_hood', False):
398
+ pieces.append(generate_hood(p))
399
+ elif garment_type in ['hoodie', 'sweatshirt']:
400
+ p = {**params, 'bodice_length': params.get('jacket_length', 65)}
401
+ pieces = [generate_bodice_front(p), generate_bodice_back(p), generate_sleeve(p), generate_hood(p)]
402
+ if params.get('has_pockets', True):
403
+ p['pocket_type'] = 'patch'
404
+ pieces.append(generate_pocket(p))
405
+ elif garment_type in ['vest']:
406
+ p = {**params, 'bodice_length': params.get('vest_length', 55)}
407
+ pieces = [generate_bodice_front(p), generate_bodice_back(p)]
408
+ else:
409
+ pieces = [generate_bodice_front(params), generate_bodice_back(params), generate_sleeve(params)]
410
+
411
+ return pieces
412
+
413
+
414
+ def render_pattern_pieces(pieces: List[Dict], title: str = "2D Sewing Pattern") -> Image.Image:
415
+ """Render pattern pieces as a clean technical drawing."""
416
+ n = len(pieces)
417
+ if n == 0:
418
+ fig, ax = plt.subplots(1, 1, figsize=(10, 8))
419
+ ax.text(0.5, 0.5, 'No pattern pieces generated', ha='center', va='center', fontsize=16)
420
+ ax.axis('off')
421
+ buf = BytesIO()
422
+ fig.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
423
+ plt.close(fig)
424
+ buf.seek(0)
425
+ return Image.open(buf)
426
+
427
+ cols = min(3, n)
428
+ rows = math.ceil(n / cols)
429
+
430
+ fig, axes = plt.subplots(rows, cols, figsize=(7 * cols, 7 * rows))
431
+ fig.suptitle(title, fontsize=18, fontweight='bold', y=0.98)
432
+
433
+ if rows == 1 and cols == 1:
434
+ axes = np.array([axes])
435
+ axes = np.atleast_2d(axes)
436
+
437
+ for idx in range(rows * cols):
438
+ row, col = divmod(idx, cols)
439
+ ax = axes[row, col]
440
+
441
+ if idx >= n:
442
+ ax.axis('off')
443
+ continue
444
+
445
+ piece = pieces[idx]
446
+ points = piece['points']
447
+ color = PIECE_COLORS[idx % len(PIECE_COLORS)]
448
+
449
+ pts = points + [points[0]]
450
+ xs = [p[0] for p in pts]
451
+ ys = [p[1] for p in pts]
452
+
453
+ ax.fill(xs, ys, color=color, alpha=0.3, linewidth=0)
454
+ ax.plot(xs, ys, 'k-', linewidth=1.5, label='Cut line')
455
+
456
+ cx = np.mean([p[0] for p in points])
457
+ cy = np.mean([p[1] for p in points])
458
+ scale = 0.92
459
+ inner_xs = [cx + (x - cx) * scale for x in xs]
460
+ inner_ys = [cy + (y - cy) * scale for y in ys]
461
+ ax.plot(inner_xs, inner_ys, 'k--', linewidth=0.7, alpha=0.5, label='Seam line')
462
+
463
+ if 'fold_line' in piece:
464
+ fl = piece['fold_line']
465
+ ax.plot([fl[0][0], fl[1][0]], [fl[0][1], fl[1][1]], 'b-.', linewidth=1.2, label='Fold')
466
+
467
+ if 'grain_line' in piece:
468
+ gl = piece['grain_line']
469
+ ax.annotate('', xy=gl[1], xytext=gl[0],
470
+ arrowprops=dict(arrowstyle='->', color='red', lw=1.5))
471
+ mid_x = (gl[0][0] + gl[1][0]) / 2
472
+ mid_y = (gl[0][1] + gl[1][1]) / 2
473
+ ax.text(mid_x + 1, mid_y, 'GRAIN', fontsize=7, color='red', rotation=90,
474
+ ha='left', va='center', fontweight='bold')
475
+
476
+ if 'notches' in piece:
477
+ for nx, ny in piece['notches']:
478
+ ax.plot(nx, ny, 'k^', markersize=6)
479
+
480
+ qty = piece.get('quantity', 1)
481
+ qty_str = f" (×{qty})" if qty > 1 else " (×1 on fold)" if 'fold_line' in piece else ""
482
+ ax.set_title(f"{piece['name']}{qty_str}", fontsize=12, fontweight='bold', pad=10)
483
+
484
+ if 'labels' in piece:
485
+ for label, (lx, ly) in piece['labels'].items():
486
+ ax.text(lx, ly, label, fontsize=8, ha='center', va='center',
487
+ color='navy', style='italic',
488
+ bbox=dict(boxstyle='round,pad=0.2', facecolor='white', alpha=0.7, edgecolor='none'))
489
+
490
+ ax.set_aspect('equal')
491
+ ax.grid(True, alpha=0.2, linestyle=':')
492
+ ax.tick_params(labelsize=7)
493
+ ax.set_xlabel('cm', fontsize=8)
494
+ ax.set_ylabel('cm', fontsize=8)
495
+
496
+ if n > 0:
497
+ from matplotlib.lines import Line2D
498
+ legend_elements = [
499
+ Line2D([0], [0], color='k', linewidth=1.5, label='Cut line'),
500
+ Line2D([0], [0], color='k', linewidth=0.7, linestyle='--', label='Seam line'),
501
+ Line2D([0], [0], color='b', linewidth=1.2, linestyle='-.', label='Fold line'),
502
+ Line2D([0], [0], color='red', linewidth=1.5, marker='>', markersize=5, label='Grain line'),
503
+ Line2D([0], [0], color='k', linewidth=0, marker='^', markersize=6, label='Notch'),
504
+ ]
505
+ fig.legend(handles=legend_elements, loc='lower center', ncol=5, fontsize=9,
506
+ bbox_to_anchor=(0.5, 0.01), frameon=True, fancybox=True)
507
+
508
+ plt.tight_layout(rect=[0, 0.04, 1, 0.96])
509
+
510
+ buf = BytesIO()
511
+ fig.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
512
+ plt.close(fig)
513
+ buf.seek(0)
514
+ return Image.open(buf)
515
+
516
+
517
+ def generate_pattern_from_analysis(analysis: Dict) -> Tuple[Image.Image, str]:
518
+ """Main entry: generate 2D pattern from garment analysis dict."""
519
+ garment_type = analysis.get('garment_type', 'shirt')
520
+ measurements = analysis.get('measurements', {})
521
+ features = analysis.get('features', {})
522
+
523
+ params = {**measurements, **features}
524
+ pieces = get_pattern_pieces(garment_type, params)
525
+
526
+ title = f"2D Sewing Pattern — {garment_type.title()}"
527
+ pattern_image = render_pattern_pieces(pieces, title)
528
+
529
+ summary_lines = [f"## Pattern: {garment_type.title()}\n"]
530
+ summary_lines.append(f"**Total pieces:** {len(pieces)}\n")
531
+
532
+ for i, piece in enumerate(pieces):
533
+ qty = piece.get('quantity', 1)
534
+ fold = " (cut on fold)" if 'fold_line' in piece else ""
535
+ summary_lines.append(f"{i+1}. **{piece['name']}** — Cut {qty}{fold}")
536
+
537
+ summary_lines.append(f"\n### Measurements Used (cm):")
538
+ for key, val in params.items():
539
+ if isinstance(val, (int, float)):
540
+ summary_lines.append(f"- {key.replace('_', ' ').title()}: {val}")
541
+
542
+ summary_lines.append(f"\n*Pattern includes 1cm seam allowance (dashed inner line).*")
543
+ summary_lines.append(f"*Arrows indicate grain line direction.*")
544
+
545
+ return pattern_image, "\n".join(summary_lines)