vikashmakeit commited on
Commit
efe8749
·
verified ·
1 Parent(s): bf8504d

Fix 3D garment visualization: anatomically correct body, proper garment positioning and scaling

Browse files

- 17-landmark shaped body (not a simple cylinder)
- Garment radii derived from circumference/(2*pi) (not raw measurement*body_radius)
- Shirt/dress torso at Z=89-145 (chest area, not starting from feet)
- Sleeves as tilted tubes from shoulder outward/downward
- Pants legs offset from center (cx=±hip_rx*0.45)
- Skirt flares from waist downward
- Collar at neck height (Z=148-156)
- Fabric-like Plotly lighting (high ambient, low specular)
- aspectmode="data" prevents distortion

Files changed (1) hide show
  1. garment_3d.py +523 -227
garment_3d.py CHANGED
@@ -1,278 +1,574 @@
1
  """
2
- 3D Garment Visualizer - Surface Mesh Approach
3
- Creates realistic 3D garment surfaces on a parametric body using Plotly.
 
 
 
 
 
 
 
 
 
4
  """
5
  import numpy as np
6
  import plotly.graph_objects as go
7
- from typing import Dict
8
-
9
- def _make_body_surface(height=170, chest=88, waist=74, hip=96):
10
- """Create a parametric human body as a mesh surface."""
11
- n_theta = 36
12
- n_z = 50
13
- theta = np.linspace(0, 2*np.pi, n_theta)
14
- z = np.linspace(0, height, n_z)
15
- Theta, Z = np.meshgrid(theta, z)
16
- R = np.zeros_like(Z)
17
- for i in range(n_z):
18
- t = Z[i,0] / height
19
- if t < 0.08:
20
- r = 6 + t*50
21
- elif t < 0.15:
22
- f = (t-0.08)/0.07
23
- r = 10 + (chest/2*0.5 - 10)*f
24
- elif t < 0.44:
25
- f = (t-0.15)/0.29
26
- r = chest/2 * (1 - 0.05*f)
27
- elif t < 0.55:
28
- f = (t-0.44)/0.11
29
- r = chest/2*0.95 - (chest/2*0.95 - waist/2)*f
30
- elif t < 0.72:
31
- f = (t-0.55)/0.17
32
- r = waist/2 + (hip/2 - waist/2)*f
33
- else:
34
- f = (t-0.72)/0.28
35
- r = hip/2 * (1 - 0.6*f)
36
- R[i,:] = r
37
- X = R * np.cos(Theta) * 0.85
38
- Y = R * np.sin(Theta) * 0.55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  return X, Y, Z
40
 
41
- def _garment_surface_shirt(params):
42
- """Create shirt/t-shirt/blouse as 3D surface mesh."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  bust = params.get("bust", 92)
44
  waist = params.get("waist", 74)
45
- hip = params.get("hip", 96)
46
  shoulder = params.get("shoulder_width", 42)
47
- body_len = params.get("bodice_length", 72)
48
  sleeve_len = params.get("sleeve_length", 60)
49
- bicep_r = params.get("bicep", 30) / 2 + 2
50
- wrist_r = params.get("wrist", 18) / 2 + 1
51
- flare = params.get("flare", 0)
52
  fit = params.get("fit", "regular")
53
- ease = {"fitted": 2, "regular": 4, "oversized": 10, "loose": 8}.get(fit, 4)
54
- n_theta = 36
55
- theta = np.linspace(0, 2*np.pi, n_theta)
 
 
 
 
 
 
 
56
  surfaces = []
57
 
58
- # Torso - main body of shirt
59
- n_z_torso = 30
60
- z_torso = np.linspace(0, body_len, n_z_torso)
61
- Theta_t, Z_t = np.meshgrid(theta, z_torso)
62
- R_t = np.zeros_like(Z_t)
63
- for i in range(n_z_torso):
64
- t = z_torso[i] / body_len
 
 
 
 
65
  if t < 0.25:
66
- r = (shoulder/2 + ease) * (1 - 0.15*t)
 
67
  elif t < 0.55:
68
- f = (t-0.25)/0.3
69
- r_top = (shoulder/2 + ease) * 0.962
70
- r_bot = (bust/2 + ease)
71
- r = r_top + (r_bot - r_top) * f
72
- elif t < 0.7:
73
- f = (t-0.55)/0.15
74
- r = (bust/2 + ease) * (1 - 0.05*f)
75
  else:
76
- f = (t-0.7)/0.3
77
- r_bust = (bust/2 + ease) * 0.95
78
- r_waist = (waist/2 + ease + flare)
79
- r = r_bust + (r_waist - r_bust) * f
80
- R_t[i,:] = r
81
- X_t = R_t * np.cos(Theta_t) * 0.85
82
- Y_t = R_t * np.sin(Theta_t) * 0.55
83
- surfaces.append((X_t, Y_t, Z_t, "#5B9BD5", "Torso", 0.7))
84
-
85
- # Sleeves
 
 
 
 
 
 
 
 
 
 
 
86
  for side in [1, -1]:
87
- n_sl = 15
88
- z_sl = np.linspace(0, sleeve_len, n_sl)
89
- n_th_sl = 16
90
- th_sl = np.linspace(0, 2*np.pi, n_th_sl)
91
- Theta_s, Z_s = np.meshgrid(th_sl, z_sl)
92
- R_s = np.zeros_like(Z_s)
93
- for j in range(n_sl):
94
- t = z_sl[j] / sleeve_len
95
- R_s[j,:] = bicep_r * (1-t) + wrist_r * t + ease*0.5
96
- sx_center = side * (shoulder*0.85 + ease*0.3)
97
- sy_center = 0
98
- sz_top = body_len * 0.92
99
- X_s = sx_center + R_s * np.cos(Theta_s) * 0.7 + side * np.linspace(0, sleeve_len*0.15, n_sl).reshape(-1,1)
100
- Y_s = sy_center + R_s * np.sin(Theta_s) * 0.5 + np.linspace(0, -sleeve_len*0.4, n_sl).reshape(-1,1)
101
- Z_s = sz_top - Z_s * 0.3
102
- color = "#6BB3E0" if side > 0 else "#4A90D9"
103
- surfaces.append((X_s, Y_s, Z_s, color, "Sleeve", 0.65))
104
-
105
- # Collar
106
  if params.get("has_collar", False):
107
  collar_h = params.get("collar_height", 4)
108
- neck_r = params.get("neckline_width", 7) + 1
109
- n_c = 24
110
- th_c = np.linspace(0, 2*np.pi, n_c)
111
- n_zc = 5
112
- z_c = np.linspace(0, collar_h, n_zc)
113
- Theta_c, Z_c = np.meshgrid(th_c, z_c)
114
- R_c = np.ones_like(Z_c) * neck_r
115
- X_c = R_c * np.cos(Theta_c) * 0.85
116
- Y_c = R_c * np.sin(Theta_c) * 0.55
117
- Z_c = Z_c + body_len - 2
118
- surfaces.append((X_c, Y_c, Z_c, "#FFFFFF", "Collar", 0.9))
119
 
120
  return surfaces
121
- def _garment_surface_pants(params):
122
- """Create pants/trousers as 3D surface mesh."""
 
 
123
  waist = params.get("waist", 74)
124
  hip = params.get("hip", 96)
125
  thigh = params.get("thigh", 56)
126
  ankle = params.get("ankle", 24)
127
  pant_len = params.get("pant_length", 100)
128
- crotch = params.get("crotch_depth", 27)
129
  fit = params.get("fit", "regular")
130
- ease = {"fitted":1,"regular":3,"oversized":6,"loose":5}.get(fit, 3)
131
- body_h = 170
132
- n_theta = 24
133
- theta = np.linspace(0, 2*np.pi, n_theta)
134
  surfaces = []
135
- for side in [1, -1]:
136
- n_z = 25
137
- z = np.linspace(0, pant_len, n_z)
138
- Theta, Z = np.meshgrid(theta, z)
139
- R = np.zeros_like(Z)
140
- for i in range(n_z):
141
- t = z[i] / pant_len
142
- if t < 0.2:
143
- f = t/0.2
144
- r = (hip/2+ease) * (1-f) + (thigh/2+ease)*f
145
- elif t < 0.35:
146
- r = thigh/2 + ease
147
- else:
148
- f = (t-0.35)/0.65
149
- r = (thigh/2+ease) * (1-f) + (ankle/2+ease) * f
150
- R[i,:] = r
151
- cx = side * (hip/4 + ease*0.3)
152
- X = cx + R * np.cos(Theta) * 0.8
153
- Y = R * np.sin(Theta) * 0.55
154
- Z = (body_h * 0.58 - crotch * 0.3) - Z * 0.3 + Z * 0.7
155
- color = "#2C5F8A" if side > 0 else "#3A6FA0"
156
- surfaces.append((X, Y, Z, color, "Leg", 0.7))
157
- # Waistband
158
- n_wb = 24
159
- th_wb = np.linspace(0, 2*np.pi, n_wb)
160
- n_zw = 4
161
- z_wb = np.linspace(0, params.get("waistband_height", 4), n_zw)
162
- Theta_w, Z_w = np.meshgrid(th_wb, z_wb)
163
- R_w = np.ones_like(Z_w) * (waist/2 + ease)
164
- X_w = R_w * np.cos(Theta_w) * 0.85
165
- Y_w = R_w * np.sin(Theta_w) * 0.55
166
- Z_w = Z_w + body_h * 0.58
167
- surfaces.append((X_w, Y_w, Z_w, "#1A3D5C", "Waistband", 0.85))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  return surfaces
169
 
170
- def _garment_surface_skirt(params):
171
- """Create skirt as 3D surface mesh."""
172
- waist = params.get("waist", 74)
 
173
  hip = params.get("hip", 96)
174
  skirt_len = params.get("skirt_length", 55)
175
  flare = params.get("flare", 5)
176
- body_h = 170
177
- n_theta = 36
178
- theta = np.linspace(0, 2*np.pi, n_theta)
179
- n_z = 25
180
- z = np.linspace(0, skirt_len, n_z)
181
- Theta, Z = np.meshgrid(theta, z)
182
- R = np.zeros_like(Z)
183
- for i in range(n_z):
184
- t = z[i] / skirt_len
 
 
 
 
 
 
 
 
 
 
 
 
185
  if t < 0.3:
186
  f = t / 0.3
187
- r = (waist/2+2) * (1-f) + (hip/2+2) * f
 
188
  else:
189
- f = (t-0.3)/0.7
190
- r = (hip/2+2) + flare * f * (1 + 0.5*np.sin(3*theta)**2)[np.newaxis,:]
191
- R[i,:] = r
192
- X = R * np.cos(Theta) * 0.85
193
- Y = R * np.sin(Theta) * 0.55
194
- waist_z = body_h * 0.58
195
- Z = waist_z - Z
196
- return [(X, Y, Z, "#C48BB8", "Skirt", 0.7)]
197
- def _garment_surface_dress(params):
198
- return _garment_surface_shirt(params) + _garment_surface_skirt(params)
199
-
200
- def _garment_surface_hoodie(params):
201
- surfaces = _garment_surface_shirt(params)
202
- head_r = params.get("head_circumference", 57) / 2
203
- n_th = 24
204
- theta = np.linspace(0, 2*np.pi, n_th)
205
- n_z = 15
206
- z = np.linspace(0, head_r*1.3, n_z)
207
- Theta, Z = np.meshgrid(theta, z)
208
- R = np.zeros_like(Z)
209
- for i in range(n_z):
210
- t = z[i] / (head_r*1.3)
211
- if t < 0.3:
212
- R[i,:] = head_r * (0.8 + 0.2*t)
213
- elif t < 0.7:
214
- R[i,:] = head_r * (1.0 - 0.1*(t-0.3)/0.4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  else:
216
- R[i,:] = head_r * 0.9 * (1 - 0.5*(t-0.7)/0.3)
217
- body_len = params.get("bodice_length", 65)
218
- body_h = 170
219
- X = R * np.cos(Theta) * 0.85
220
- Y = R * np.sin(Theta) * 0.55 + np.linspace(0, head_r*0.3, n_z).reshape(-1,1)
221
- Z = Z + body_h * 0.92
222
- surfaces.append((X, Y, Z, "#3A7BD5", "Hood", 0.65))
 
 
 
 
 
223
  return surfaces
224
 
225
- def _garment_surface_jacket(params):
226
- return _garment_surface_shirt(params)
227
-
228
- GARMENT_SURFACE_FUNCS = {
229
- "shirt": _garment_surface_shirt, "blouse": _garment_surface_shirt,
230
- "top": _garment_surface_shirt, "t-shirt": _garment_surface_shirt, "tee": _garment_surface_shirt,
231
- "dress": _garment_surface_dress,
232
- "skirt": _garment_surface_skirt,
233
- "pants": _garment_surface_pants, "trousers": _garment_surface_pants, "jeans": _garment_surface_pants,
234
- "jacket": _garment_surface_jacket, "coat": _garment_surface_jacket, "blazer": _garment_surface_jacket,
235
- "hoodie": _garment_surface_hoodie, "sweatshirt": _garment_surface_hoodie,
236
- "vest": _garment_surface_shirt,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  def create_3d_figure(analysis: Dict) -> go.Figure:
239
- """Create interactive 3D garment visualization with surface meshes."""
240
  garment_type = analysis.get("garment_type", "shirt").lower()
241
  measurements = analysis.get("measurements", {})
242
  features = analysis.get("features", {})
243
  params = {**measurements, **features}
244
- chest = params.get("bust", 92)
245
- waist_m = params.get("waist", 74)
246
- hip_m = params.get("hip", 96)
247
  fig = go.Figure()
248
- # Body - translucent skin-toned surface
249
- BX, BY, BZ = _make_body_surface(chest=chest, waist=waist_m, hip=hip_m)
250
- fig.add_trace(go.Surface(
251
- x=BX, y=BY, z=BZ,
252
- surfacecolor=np.ones_like(BX) * 0.8,
253
- colorscale=[[0,"#E8D0B0"],[1,"#E8D0B0"]],
254
- opacity=0.25, showscale=False, name="Body",
255
- hoverinfo="skip", lighting=dict(ambient=0.9, diffuse=0.1, specular=0)))
256
  # Garment surfaces
257
- func = GARMENT_SURFACE_FUNCS.get(garment_type, _garment_surface_shirt)
258
- surfaces = func(params)
259
- for X, Y, Z, color, name, opacity in surfaces:
260
- fig.add_trace(go.Surface(
261
- x=X, y=Y, z=Z,
262
- surfacecolor=np.ones_like(X) * 0.5,
263
- colorscale=[[0, color], [1, color]],
264
- opacity=opacity, showscale=False, name=name,
265
- hoverinfo="name",
266
- lighting=dict(ambient=0.6, diffuse=0.4, specular=0.2, roughness=0.5)))
267
  fig.update_layout(
268
  scene=dict(
269
- xaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False),
270
- yaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False),
271
- zaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False),
 
 
 
272
  aspectmode="data",
273
- camera=dict(eye=dict(x=1.8, y=0.8, z=0.9), center=dict(x=0, y=0, z=-0.15))),
274
- margin=dict(l=0, r=0, t=30, b=0),
275
- height=500,
 
 
 
 
 
276
  title=dict(text=f"3D Preview: {garment_type.title()}", font=dict(size=14)),
277
- paper_bgcolor="#fafafa")
278
- return fig
 
 
 
1
  """
2
+ 3D Garment Visualizer Anatomically Correct Mannequin + Garment Surfaces
3
+
4
+ Creates realistic 3D garment previews on a shaped parametric body using Plotly.
5
+ Uses two core primitives:
6
+ 1. Revolution surfaces (body, torso, skirt, collar, waistband)
7
+ 2. Tilted tubes (sleeves)
8
+
9
+ All Z values are in cm, Z=0 at floor, Z=170 at head top.
10
+ Garment radii are derived from circumference measurements:
11
+ radius = circumference / (2*pi) (for circular cross-section)
12
+ For elliptical: rx = circ / (2*pi) * 1.1, ry = circ / (2*pi) * 0.9
13
  """
14
  import numpy as np
15
  import plotly.graph_objects as go
16
+ from typing import Dict, List, Tuple
17
+
18
+ TWO_PI = 2 * np.pi
19
+
20
+ # ── Anatomical body profile (female mannequin, 170 cm) ─────────────────────
21
+ # 17 landmarks from feet to head (all radii in cm)
22
+ _BODY_Z = [0, 7, 30, 45, 65, 88, 100, 104, 112, 120, 132, 140, 145, 150, 158, 162, 170]
23
+ _BODY_RX = [4.0, 3.0, 4.5, 4.0, 6.5, 9.5, 8.0, 7.0, 8.5, 10.0, 9.0, 8.5, 11.5, 3.5, 3.0, 5.5, 4.5]
24
+ _BODY_RY = [3.0, 2.5, 4.0, 4.0, 6.0, 8.5, 7.5, 6.5, 8.0, 9.0, 8.0, 7.5, 8.0, 3.5, 3.0, 5.0, 4.5]
25
+
26
+ # Key anatomical Z heights (cm from floor)
27
+ Z_FLOOR = 0
28
+ Z_ANKLE = 7
29
+ Z_KNEE = 45
30
+ Z_HIP = 88
31
+ Z_WAIST = 104
32
+ Z_BUST = 120
33
+ Z_SHOULDER = 145
34
+ Z_NECK_BASE = 150
35
+ Z_HEAD_TOP = 170
36
+
37
+
38
+ # ── Helpers: circumference elliptical radii ──────────────────────────────
39
+ def _circ_to_rx(circ):
40
+ """Convert circumference to X-radius (wider front-back)."""
41
+ return circ / TWO_PI * 1.10
42
+
43
+ def _circ_to_ry(circ):
44
+ """Convert circumference to Y-radius (narrower side-side)."""
45
+ return circ / TWO_PI * 0.90
46
+
47
+
48
+ # ── Primitive 1: Revolution surface ─────────────────────────────────────────
49
+ def _revolution_surface(z_heights, rx_vals, ry_vals,
50
+ n_theta=36, cx=0.0, cy=0.0):
51
+ """Surface of revolution with elliptical cross-sections."""
52
+ theta = np.linspace(0, 2 * np.pi, n_theta)
53
+ z_arr = np.asarray(z_heights, float)
54
+ rx = np.asarray(rx_vals, float)
55
+ ry = np.asarray(ry_vals, float)
56
+ Z = np.outer(z_arr, np.ones(n_theta))
57
+ X = cx + np.outer(rx, np.cos(theta))
58
+ Y = cy + np.outer(ry, np.sin(theta))
59
+ return X, Y, Z
60
+
61
+
62
+ # ── Primitive 2: Tilted tube (for sleeves) ──────────────────────────────────
63
+ def _tilted_tube(start, end, r_start, r_end, n_theta=24, n_z=12):
64
+ """Tapered tube between two 3D points."""
65
+ s = np.array(start, float)
66
+ e = np.array(end, float)
67
+ axis = e - s
68
+ length = np.linalg.norm(axis)
69
+ if length < 0.01:
70
+ return np.zeros((n_z, n_theta)), np.zeros((n_z, n_theta)), np.zeros((n_z, n_theta))
71
+
72
+ axis_n = axis / length
73
+ ref = np.array([0., 0., 1.]) if abs(axis_n[2]) < 0.9 else np.array([1., 0., 0.])
74
+ right = np.cross(axis_n, ref)
75
+ right /= np.linalg.norm(right)
76
+ up = np.cross(right, axis_n)
77
+
78
+ theta = np.linspace(0, 2 * np.pi, n_theta)
79
+ t_vals = np.linspace(0, 1, n_z)
80
+
81
+ X = np.zeros((n_z, n_theta))
82
+ Y = np.zeros((n_z, n_theta))
83
+ Z = np.zeros((n_z, n_theta))
84
+
85
+ for i, t in enumerate(t_vals):
86
+ center = s + t * axis
87
+ r = r_start + t * (r_end - r_start)
88
+ for j, th in enumerate(theta):
89
+ offset = r * (np.cos(th) * right + np.sin(th) * up)
90
+ X[i, j] = center[0] + offset[0]
91
+ Y[i, j] = center[1] + offset[1]
92
+ Z[i, j] = center[2] + offset[2]
93
+
94
  return X, Y, Z
95
 
96
+
97
+ # ── Surface helper ──────────────────────────────────────────────────────────
98
+ def _make_surface(X, Y, Z, color, name, opacity=0.88, lighting=None):
99
+ """Create a Plotly Surface trace with fabric-like shading."""
100
+ if lighting is None:
101
+ lighting = dict(ambient=0.65, diffuse=0.85, specular=0.15, roughness=0.8)
102
+ return go.Surface(
103
+ x=X, y=Y, z=Z,
104
+ surfacecolor=np.ones_like(X),
105
+ colorscale=[[0, color], [1, color]],
106
+ opacity=opacity,
107
+ showscale=False,
108
+ name=name,
109
+ hoverinfo="name",
110
+ lighting=lighting,
111
+ lightposition=dict(x=100, y=200, z=300),
112
+ )
113
+
114
+
115
+ # ── Body mannequin ──────────────────────────────────────────────────────────
116
+ def _make_body():
117
+ """Create the translucent mannequin body."""
118
+ X, Y, Z = _revolution_surface(_BODY_Z, _BODY_RX, _BODY_RY, n_theta=48)
119
+ return go.Surface(
120
+ x=X, y=Y, z=Z,
121
+ surfacecolor=np.ones_like(X) * 0.8,
122
+ colorscale=[[0, "#E8D0B0"], [1, "#E8D0B0"]],
123
+ opacity=0.20,
124
+ showscale=False,
125
+ name="Body",
126
+ hoverinfo="skip",
127
+ lighting=dict(ambient=0.9, diffuse=0.1, specular=0, roughness=1.0),
128
+ )
129
+
130
+
131
+ # ── Helper: interpolate body radius at arbitrary Z ──────────────────────────
132
+ def _body_rx(z_val):
133
+ return float(np.interp(z_val, _BODY_Z, _BODY_RX))
134
+
135
+ def _body_ry(z_val):
136
+ return float(np.interp(z_val, _BODY_Z, _BODY_RY))
137
+
138
+
139
+ # ── Garment builders ────────────────────────────────────────────────────────
140
+
141
+ def _build_shirt(params, color="#4A90D9"):
142
+ """Build shirt/blouse/top/t-shirt surfaces."""
143
  bust = params.get("bust", 92)
144
  waist = params.get("waist", 74)
 
145
  shoulder = params.get("shoulder_width", 42)
146
+ bodice_len = params.get("bodice_length", 72)
147
  sleeve_len = params.get("sleeve_length", 60)
148
+ bicep = params.get("bicep", 30)
149
+ wrist_circ = params.get("wrist", 18)
 
150
  fit = params.get("fit", "regular")
151
+ flare = params.get("flare", 0)
152
+
153
+ ease = {"fitted": 0.5, "regular": 1.2, "oversized": 3.0, "loose": 2.2}.get(fit, 1.2)
154
+
155
+ bust_rx = _circ_to_rx(bust) + ease
156
+ bust_ry = _circ_to_ry(bust) + ease
157
+ waist_rx = _circ_to_rx(waist) + ease
158
+ waist_ry = _circ_to_ry(waist) + ease
159
+ shoulder_rx = shoulder / 2
160
+
161
  surfaces = []
162
 
163
+ extra_length = max(0, bodice_len - 42) * 0.5
164
+ hem_z = max(Z_WAIST - extra_length, Z_HIP - 5)
165
+
166
+ n_torso = 16
167
+ torso_z = np.linspace(hem_z, Z_SHOULDER, n_torso)
168
+ torso_rx_arr = []
169
+ torso_ry_arr = []
170
+
171
+ for z in torso_z:
172
+ t = (z - hem_z) / (Z_SHOULDER - hem_z)
173
+
174
  if t < 0.25:
175
+ rx = waist_rx + flare * (0.25 - t) / 0.25
176
+ ry = waist_ry + flare * 0.8 * (0.25 - t) / 0.25
177
  elif t < 0.55:
178
+ f = (t - 0.25) / 0.30
179
+ rx = waist_rx + (bust_rx - waist_rx) * f
180
+ ry = waist_ry + (bust_ry - waist_ry) * f
181
+ elif t < 0.8:
182
+ f = (t - 0.55) / 0.25
183
+ rx = bust_rx + (bust_rx * 0.92 - bust_rx) * f
184
+ ry = bust_ry + (bust_ry * 0.92 - bust_ry) * f
185
  else:
186
+ f = (t - 0.8) / 0.2
187
+ rx = bust_rx * 0.92 + (shoulder_rx - bust_rx * 0.92) * f
188
+ ry = bust_ry * 0.92 * (1 - 0.15 * f)
189
+
190
+ rx = max(rx, _body_rx(z) + 0.8)
191
+ ry = max(ry, _body_ry(z) + 0.8)
192
+ torso_rx_arr.append(rx)
193
+ torso_ry_arr.append(ry)
194
+
195
+ X, Y, Zt = _revolution_surface(torso_z, torso_rx_arr, torso_ry_arr, n_theta=36)
196
+ surfaces.append(_make_surface(X, Y, Zt, color, "Torso", opacity=0.85))
197
+
198
+ shoulder_attach_rx = torso_rx_arr[-1]
199
+ shoulder_attach_z = Z_SHOULDER - 3
200
+
201
+ sleeve_drop = sleeve_len * 0.45
202
+ sleeve_spread = sleeve_len * 0.30
203
+
204
+ r_bicep = bicep / TWO_PI + ease * 0.4
205
+ r_wrist = wrist_circ / TWO_PI + ease * 0.3
206
+
207
  for side in [1, -1]:
208
+ start = (side * shoulder_attach_rx, 0, shoulder_attach_z)
209
+ end = (side * (shoulder_attach_rx + sleeve_spread), 0,
210
+ shoulder_attach_z - sleeve_drop)
211
+ Xs, Ys, Zs = _tilted_tube(start, end, r_bicep, r_wrist, n_theta=20, n_z=14)
212
+ sleeve_color = _lighten_color(color, 0.08) if side > 0 else _darken_color(color, 0.08)
213
+ surfaces.append(_make_surface(Xs, Ys, Zs, sleeve_color, "Sleeve", opacity=0.80))
214
+
 
 
 
 
 
 
 
 
 
 
 
 
215
  if params.get("has_collar", False):
216
  collar_h = params.get("collar_height", 4)
217
+ n_c = 6
218
+ collar_z = np.linspace(Z_NECK_BASE - 2, Z_NECK_BASE + collar_h, n_c)
219
+ collar_rx = []
220
+ collar_ry = []
221
+ for z in collar_z:
222
+ collar_rx.append(_body_rx(z) + 1.5)
223
+ collar_ry.append(_body_ry(z) + 1.5)
224
+ Xc, Yc, Zc = _revolution_surface(collar_z, collar_rx, collar_ry, n_theta=28)
225
+ surfaces.append(_make_surface(Xc, Yc, Zc, "#FFFFFF", "Collar", opacity=0.92))
 
 
226
 
227
  return surfaces
228
+
229
+
230
+ def _build_pants(params, color="#2C5F8A"):
231
+ """Build pants/trousers/jeans surfaces."""
232
  waist = params.get("waist", 74)
233
  hip = params.get("hip", 96)
234
  thigh = params.get("thigh", 56)
235
  ankle = params.get("ankle", 24)
236
  pant_len = params.get("pant_length", 100)
 
237
  fit = params.get("fit", "regular")
238
+ flare = params.get("flare", 0)
239
+
240
+ ease = {"fitted": 0.5, "regular": 1.0, "oversized": 2.5, "loose": 2.0}.get(fit, 1.0)
241
+
242
  surfaces = []
243
+
244
+ hip_rx = _circ_to_rx(hip) + ease
245
+ hip_ry = _circ_to_ry(hip) + ease
246
+ waist_rx = _circ_to_rx(waist) + ease
247
+ waist_ry = _circ_to_ry(waist) + ease
248
+
249
+ n_wb = 8
250
+ wb_z = np.linspace(Z_HIP, Z_WAIST, n_wb)
251
+ wb_rx_arr = []
252
+ wb_ry_arr = []
253
+ for z in wb_z:
254
+ t = (z - Z_HIP) / (Z_WAIST - Z_HIP)
255
+ rx = hip_rx + (waist_rx - hip_rx) * t
256
+ ry = hip_ry + (waist_ry - hip_ry) * t
257
+ rx = max(rx, _body_rx(z) + 0.8)
258
+ ry = max(ry, _body_ry(z) + 0.8)
259
+ wb_rx_arr.append(rx)
260
+ wb_ry_arr.append(ry)
261
+
262
+ Xw, Yw, Zw = _revolution_surface(wb_z, wb_rx_arr, wb_ry_arr, n_theta=36)
263
+ surfaces.append(_make_surface(Xw, Yw, Zw, _darken_color(color, 0.12), "Waistband", opacity=0.90))
264
+
265
+ hem_z = max(Z_HIP - pant_len * 0.85, Z_ANKLE - 5)
266
+ hem_z = max(hem_z, 2)
267
+
268
+ leg_cx = hip_rx * 0.45
269
+
270
+ r_thigh = thigh / TWO_PI + ease
271
+ r_ankle = ankle / TWO_PI + ease + flare * 0.15
272
+
273
+ n_leg = 18
274
+ leg_z = np.linspace(Z_HIP, hem_z, n_leg)
275
+
276
+ for side_idx, side in enumerate([1, -1]):
277
+ leg_rx = []
278
+ leg_ry = []
279
+ for i, z in enumerate(leg_z):
280
+ t = i / (n_leg - 1)
281
+ rx = r_thigh + (r_ankle - r_thigh) * t
282
+ ry = rx * 0.85
283
+ leg_rx.append(rx)
284
+ leg_ry.append(ry)
285
+
286
+ cx = side * leg_cx
287
+ Xl, Yl, Zl = _revolution_surface(leg_z, leg_rx, leg_ry, n_theta=28, cx=cx)
288
+ leg_color = color if side_idx == 0 else _lighten_color(color, 0.06)
289
+ surfaces.append(_make_surface(Xl, Yl, Zl, leg_color, "Leg", opacity=0.85))
290
+
291
  return surfaces
292
 
293
+
294
+ def _build_skirt(params, color="#C48BB8"):
295
+ """Build skirt surfaces."""
296
+ waist = params.get("waist", 72)
297
  hip = params.get("hip", 96)
298
  skirt_len = params.get("skirt_length", 55)
299
  flare = params.get("flare", 5)
300
+ fit = params.get("fit", "regular")
301
+
302
+ ease = {"fitted": 0.5, "regular": 1.0, "oversized": 2.5, "loose": 2.0}.get(fit, 1.0)
303
+
304
+ surfaces = []
305
+
306
+ hem_z = max(Z_WAIST - skirt_len, Z_ANKLE - 5)
307
+ hem_z = max(hem_z, 2)
308
+
309
+ waist_rx = _circ_to_rx(waist) + ease
310
+ waist_ry = _circ_to_ry(waist) + ease
311
+ hip_rx = _circ_to_rx(hip) + ease
312
+ hip_ry = _circ_to_ry(hip) + ease
313
+
314
+ n_sk = 16
315
+ skirt_z = np.linspace(Z_WAIST, hem_z, n_sk)
316
+ skirt_rx = []
317
+ skirt_ry = []
318
+
319
+ for i, z in enumerate(skirt_z):
320
+ t = i / (n_sk - 1)
321
  if t < 0.3:
322
  f = t / 0.3
323
+ rx = waist_rx + (hip_rx - waist_rx) * f
324
+ ry = waist_ry + (hip_ry - waist_ry) * f
325
  else:
326
+ f = (t - 0.3) / 0.7
327
+ rx = hip_rx + flare * f
328
+ ry = hip_ry + flare * 0.85 * f
329
+
330
+ rx = max(rx, _body_rx(z) + 1.0)
331
+ ry = max(ry, _body_ry(z) + 1.0)
332
+ skirt_rx.append(rx)
333
+ skirt_ry.append(ry)
334
+
335
+ Xs, Ys, Zs = _revolution_surface(skirt_z, skirt_rx, skirt_ry, n_theta=36)
336
+ surfaces.append(_make_surface(Xs, Ys, Zs, color, "Skirt", opacity=0.82))
337
+
338
+ n_wb = 4
339
+ wb_height = params.get("waistband_height", 4)
340
+ wb_z = np.linspace(Z_WAIST, Z_WAIST + wb_height, n_wb)
341
+ wb_rx = [waist_rx + 0.3] * n_wb
342
+ wb_ry = [waist_ry + 0.3] * n_wb
343
+ Xwb, Ywb, Zwb = _revolution_surface(wb_z, wb_rx, wb_ry, n_theta=36)
344
+ surfaces.append(_make_surface(Xwb, Ywb, Zwb, _darken_color(color, 0.15), "Waistband", opacity=0.92))
345
+
346
+ return surfaces
347
+
348
+
349
+ def _build_dress(params, color="#8E44AD"):
350
+ """Build dress = bodice top + skirt bottom."""
351
+ surfaces = []
352
+ bodice_params = dict(params)
353
+ bodice_params["bodice_length"] = params.get("bodice_length", 42)
354
+ surfaces.extend(_build_shirt(bodice_params, color=color))
355
+
356
+ skirt_params = dict(params)
357
+ skirt_params["skirt_length"] = params.get("skirt_length", 55)
358
+ surfaces.extend(_build_skirt(skirt_params, color=_lighten_color(color, 0.05)))
359
+
360
+ return surfaces
361
+
362
+
363
+ def _build_hoodie(params, color="#27AE60"):
364
+ """Build hoodie = shirt + hood."""
365
+ surfaces = _build_shirt(params, color=color)
366
+
367
+ n_hood = 10
368
+ hood_z = np.linspace(Z_SHOULDER, Z_HEAD_TOP + 2, n_hood)
369
+ hood_rx = []
370
+ hood_ry = []
371
+
372
+ for i, z in enumerate(hood_z):
373
+ t = i / (n_hood - 1)
374
+ if t < 0.2:
375
+ rx = _body_rx(Z_SHOULDER) + 2.5
376
+ elif t < 0.6:
377
+ f = (t - 0.2) / 0.4
378
+ rx = _body_rx(min(z, Z_HEAD_TOP)) + 3.0 + 2.0 * np.sin(f * np.pi)
379
+ else:
380
+ f = (t - 0.6) / 0.4
381
+ rx = _body_rx(min(z, Z_HEAD_TOP)) + 3.0 * (1 - f * 0.6)
382
+ hood_rx.append(rx)
383
+ hood_ry.append(rx * 0.85)
384
+
385
+ Xh, Yh, Zh = _revolution_surface(hood_z, hood_rx, hood_ry, n_theta=28)
386
+ surfaces.append(_make_surface(Xh, Yh, Zh, _darken_color(color, 0.12), "Hood", opacity=0.78))
387
+
388
+ return surfaces
389
+
390
+
391
+ def _build_jacket(params, color="#7F8C8D"):
392
+ """Build jacket/coat/blazer — like a shirt but longer, wider."""
393
+ jacket_params = dict(params)
394
+ jacket_len = params.get("jacket_length", params.get("bodice_length", 70))
395
+ jacket_params["bodice_length"] = jacket_len
396
+
397
+ original_fit = params.get("fit", "regular")
398
+ if original_fit == "fitted":
399
+ jacket_params["fit"] = "regular"
400
+ elif original_fit == "regular":
401
+ jacket_params["fit"] = "loose"
402
+
403
+ surfaces = _build_shirt(jacket_params, color=color)
404
+
405
+ if params.get("has_hood", False):
406
+ surfaces.extend(_build_hood_only(params, color))
407
+
408
+ return surfaces
409
+
410
+
411
+ def _build_hood_only(params, color):
412
+ """Just the hood part."""
413
+ n_hood = 10
414
+ hood_z = np.linspace(Z_SHOULDER, Z_HEAD_TOP + 2, n_hood)
415
+ hood_rx = []
416
+ hood_ry = []
417
+
418
+ for i, z in enumerate(hood_z):
419
+ t = i / (n_hood - 1)
420
+ if t < 0.2:
421
+ rx = _body_rx(Z_SHOULDER) + 2.5
422
+ elif t < 0.6:
423
+ f = (t - 0.2) / 0.4
424
+ rx = _body_rx(min(z, Z_HEAD_TOP)) + 3.0 + 2.0 * np.sin(f * np.pi)
425
+ else:
426
+ f = (t - 0.6) / 0.4
427
+ rx = _body_rx(min(z, Z_HEAD_TOP)) + 3.0 * (1 - f * 0.6)
428
+ hood_rx.append(rx)
429
+ hood_ry.append(rx * 0.85)
430
+
431
+ Xh, Yh, Zh = _revolution_surface(hood_z, hood_rx, hood_ry, n_theta=28)
432
+ return [_make_surface(Xh, Yh, Zh, _darken_color(color, 0.12), "Hood", opacity=0.78)]
433
+
434
+
435
+ def _build_vest(params, color="#D4A574"):
436
+ """Build vest — sleeveless bodice."""
437
+ bust = params.get("bust", 92)
438
+ waist = params.get("waist", 74)
439
+ shoulder = params.get("shoulder_width", 42)
440
+ fit = params.get("fit", "regular")
441
+ flare = params.get("flare", 0)
442
+
443
+ ease = {"fitted": 0.5, "regular": 1.2, "oversized": 3.0, "loose": 2.2}.get(fit, 1.2)
444
+
445
+ bust_rx = _circ_to_rx(bust) + ease
446
+ bust_ry = _circ_to_ry(bust) + ease
447
+ waist_rx = _circ_to_rx(waist) + ease
448
+ waist_ry = _circ_to_ry(waist) + ease
449
+ shoulder_rx = shoulder / 2
450
+
451
+ surfaces = []
452
+ vest_len = params.get("vest_length", params.get("bodice_length", 55))
453
+ extra = max(0, vest_len - 42) * 0.5
454
+ hem_z = max(Z_WAIST - extra, Z_HIP)
455
+
456
+ n = 14
457
+ torso_z = np.linspace(hem_z, Z_SHOULDER, n)
458
+ torso_rx = []
459
+ torso_ry = []
460
+
461
+ for z in torso_z:
462
+ t = (z - hem_z) / (Z_SHOULDER - hem_z)
463
+ if t < 0.25:
464
+ rx = waist_rx + flare * (0.25 - t) / 0.25
465
+ ry = waist_ry + flare * 0.8 * (0.25 - t) / 0.25
466
+ elif t < 0.55:
467
+ f = (t - 0.25) / 0.30
468
+ rx = waist_rx + (bust_rx - waist_rx) * f
469
+ ry = waist_ry + (bust_ry - waist_ry) * f
470
+ elif t < 0.8:
471
+ f = (t - 0.55) / 0.25
472
+ rx = bust_rx * (1 - 0.08 * f)
473
+ ry = bust_ry * (1 - 0.08 * f)
474
  else:
475
+ f = (t - 0.8) / 0.2
476
+ rx = bust_rx * 0.92 + (shoulder_rx - bust_rx * 0.92) * f
477
+ ry = bust_ry * 0.92 * (1 - 0.15 * f)
478
+
479
+ rx = max(rx, _body_rx(z) + 0.8)
480
+ ry = max(ry, _body_ry(z) + 0.8)
481
+ torso_rx.append(rx)
482
+ torso_ry.append(ry)
483
+
484
+ X, Y, Zt = _revolution_surface(torso_z, torso_rx, torso_ry, n_theta=36)
485
+ surfaces.append(_make_surface(X, Y, Zt, color, "Vest", opacity=0.85))
486
+
487
  return surfaces
488
 
489
+
490
+ # ── Color helpers ───────────────────────────────────────────────────────────
491
+ def _hex_to_rgb(hex_color):
492
+ hex_color = hex_color.lstrip('#')
493
+ return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
494
+
495
+ def _rgb_to_hex(r, g, b):
496
+ return f"#{int(r):02x}{int(g):02x}{int(b):02x}"
497
+
498
+ def _lighten_color(hex_color, amount=0.1):
499
+ r, g, b = _hex_to_rgb(hex_color)
500
+ return _rgb_to_hex(min(255, r + int(255 * amount)),
501
+ min(255, g + int(255 * amount)),
502
+ min(255, b + int(255 * amount)))
503
+
504
+ def _darken_color(hex_color, amount=0.1):
505
+ r, g, b = _hex_to_rgb(hex_color)
506
+ return _rgb_to_hex(max(0, r - int(255 * amount)),
507
+ max(0, g - int(255 * amount)),
508
+ max(0, b - int(255 * amount)))
509
+
510
+
511
+ # ── Default garment colors ─────────────────────────────────────────────────
512
+ GARMENT_COLORS = {
513
+ "shirt": "#4A90D9", "blouse": "#D98CB3", "top": "#5B9BD5",
514
+ "t-shirt": "#4A90D9", "tee": "#4A90D9",
515
+ "dress": "#8E44AD", "skirt": "#C48BB8",
516
+ "pants": "#2C5F8A", "trousers": "#2C5F8A", "jeans": "#1A3D6C",
517
+ "jacket": "#7F8C8D", "coat": "#5D6D7E", "blazer": "#6C757D",
518
+ "hoodie": "#27AE60", "sweatshirt": "#27AE60",
519
+ "vest": "#D4A574",
520
  }
521
+
522
+ GARMENT_BUILDERS = {
523
+ "shirt": _build_shirt, "blouse": _build_shirt,
524
+ "top": _build_shirt, "t-shirt": _build_shirt, "tee": _build_shirt,
525
+ "dress": _build_dress, "skirt": _build_skirt,
526
+ "pants": _build_pants, "trousers": _build_pants, "jeans": _build_pants,
527
+ "jacket": _build_jacket, "coat": _build_jacket, "blazer": _build_jacket,
528
+ "hoodie": _build_hoodie, "sweatshirt": _build_hoodie,
529
+ "vest": _build_vest,
530
+ }
531
+
532
+
533
+ # ── Main entry point ───────────────────────────────────────────────────────
534
  def create_3d_figure(analysis: Dict) -> go.Figure:
535
+ """Create interactive 3D garment visualization on a mannequin body."""
536
  garment_type = analysis.get("garment_type", "shirt").lower()
537
  measurements = analysis.get("measurements", {})
538
  features = analysis.get("features", {})
539
  params = {**measurements, **features}
540
+
 
 
541
  fig = go.Figure()
542
+
543
+ # Mannequin body
544
+ fig.add_trace(_make_body())
545
+
 
 
 
 
546
  # Garment surfaces
547
+ color = GARMENT_COLORS.get(garment_type, "#4A90D9")
548
+ builder = GARMENT_BUILDERS.get(garment_type, _build_shirt)
549
+ for surf in builder(params, color=color):
550
+ fig.add_trace(surf)
551
+
552
+ # Scene layout
 
 
 
 
553
  fig.update_layout(
554
  scene=dict(
555
+ xaxis=dict(showgrid=False, showticklabels=False, title="",
556
+ zeroline=False, range=[-35, 35]),
557
+ yaxis=dict(showgrid=False, showticklabels=False, title="",
558
+ zeroline=False, range=[-35, 35]),
559
+ zaxis=dict(showgrid=False, showticklabels=False, title="",
560
+ zeroline=False, range=[0, 180]),
561
  aspectmode="data",
562
+ camera=dict(
563
+ eye=dict(x=1.8, y=1.2, z=0.5),
564
+ center=dict(x=0, y=0, z=-0.1),
565
+ ),
566
+ bgcolor="rgba(245,245,248,1)",
567
+ ),
568
+ margin=dict(l=0, r=0, t=35, b=0),
569
+ height=550,
570
  title=dict(text=f"3D Preview: {garment_type.title()}", font=dict(size=14)),
571
+ paper_bgcolor="#fafafa",
572
+ )
573
+
574
+ return fig