vikashmakeit commited on
Commit
75e627e
·
verified ·
1 Parent(s): 35d2c31

Rewrite 3D visualizer with proper surface meshes (not wireframes)

Browse files
Files changed (1) hide show
  1. garment_3d.py +247 -181
garment_3d.py CHANGED
@@ -1,212 +1,278 @@
1
  """
2
- 3D Garment Visualizer
3
- Generates a 3D garment mesh on a parametric body using Plotly.
4
  """
5
  import numpy as np
6
  import plotly.graph_objects as go
7
- from typing import Dict, Optional
8
 
9
- def create_body_mesh(height=170, chest=88, waist=74, hip=96):
10
- theta = np.linspace(0, 2*np.pi, 30)
11
- z_levels = np.linspace(0, height, 40)
12
- vertices = []
13
- faces = []
14
- for i, z in enumerate(z_levels):
15
- t = z / height
16
- if t < 0.12:
17
- r = 8 + t * 40
18
- elif t < 0.20:
19
- r = chest/2 * 0.3 + (t-0.12)/0.08 * (chest/2*0.7-chest/2*0.3)
20
- elif t < 0.45:
21
- frac = (t-0.20)/0.25
22
- r = chest/2 * (1 - 0.08*frac)
 
 
 
 
23
  elif t < 0.55:
24
- r = waist/2
 
25
  elif t < 0.72:
26
- frac = (t-0.55)/0.17
27
- r = waist/2 + (hip/2 - waist/2) * frac
28
  else:
29
- frac = (t-0.72)/0.28
30
- r = hip/2 * (1 - 0.5*frac)
31
- for j, th in enumerate(theta):
32
- x = r * np.cos(th) * 0.85
33
- y = r * np.sin(th) * 0.5
34
- vertices.append([x, y, z])
35
- vertices = np.array(vertices)
36
- n_theta = len(theta)
37
- for i in range(len(z_levels)-1):
38
- for j in range(n_theta-1):
39
- v0 = i*n_theta + j
40
- v1 = i*n_theta + j+1
41
- v2 = (i+1)*n_theta + j
42
- v3 = (i+1)*n_theta + j+1
43
- faces.append([v0, v2, v1])
44
- faces.append([v1, v2, v3])
45
- return vertices, faces
46
 
47
- def _garment_mesh_shirt(params, body_verts, n_theta, z_levels):
48
- parts = []
49
- bust = params.get('bust', 92)/2
50
- waist = params.get('waist', 74)/2
51
- shoulder = params.get('shoulder_width', 42)/2
52
- body_len = params.get('bodice_length', 72)
53
- sleeve_len = params.get('sleeve_length', 60)
54
- sleeve_lo = params.get('bicep', 30)/2 + 2
55
- sleeve_hi = params.get('wrist', 18)/2 + 1
56
- # Front panel
57
- front_z = np.linspace(0, body_len, 20)
58
- front_x = []
59
- front_y = []
60
- for z in front_z:
61
- t = z / body_len
62
- if t < 0.3:
63
- x = shoulder * (1 - 0.1*t)
 
 
 
 
 
 
 
 
 
64
  elif t < 0.55:
65
- x = shoulder - (shoulder - bust*0.85) * ((t-0.3)/0.25)
 
 
 
 
 
 
66
  else:
67
- x = bust*0.85 - (bust*0.85 - waist*0.9) * ((t-0.55)/0.45)
68
- front_x.append(x)
69
- front_y.append(0)
70
- pts = list(zip(front_x, front_y, front_z))
71
- pts.append((0, 0, front_z[-1]))
72
- pts.insert(0, (0, 0, 0))
73
- n = len(pts)
74
- xs = [p[0] for p in pts]
75
- ys = [p[1] for p in pts]
76
- zs = [p[2] for p in pts]
77
- parts.append(dict(x=xs, y=ys, z=zs, type='scatter3d', mode='lines',
78
- line=dict(color='#4A90D9', width=4), name='Front'))
79
- # Back panel (mirror x)
80
- back_x = [-x for x in xs]
81
- parts.append(dict(x=back_x, y=ys, z=zs, type='scatter3d', mode='lines',
82
- line=dict(color='#6BB3E0', width=4), name='Back'))
83
  # Sleeves
84
- shoulder_pt = [shoulder*0.9, 0, body_len*0.92]
85
- sl_z = np.linspace(0, sleeve_len, 10)
86
  for side in [1, -1]:
87
- sx = [side * (shoulder*0.9 + (sleeve_lo - shoulder*0.3)*0.3 + sl*0.08*side) for sl in sl_z]
88
- sy = [sl*0.6 for sl in sl_z]
89
- sz = [body_len*0.92 - sl*0.15 for sl in sl_z]
90
- color = '#7BC8F6' if side > 0 else '#5BA3D9'
91
- parts.append(dict(x=sx, y=sy, z=sz, type='scatter3d', mode='lines',
92
- line=dict(color=color, width=4), name='Sleeve R' if side > 0 else 'Sleeve L'))
93
- return parts
 
 
 
 
 
 
 
 
 
 
94
 
95
- def _garment_mesh_pants(params, body_verts, n_theta, z_levels):
96
- parts = []
97
- waist_r = params.get('waist', 74)/2
98
- hip_r = params.get('hip', 96)/2
99
- pant_len = params.get('pant_length', 100)
100
- crotch = params.get('crotch_depth', 27)
101
- ankle = params.get('ankle', 24)/2 + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  body_h = 170
 
 
 
103
  for side in [1, -1]:
104
- leg_x = [side * waist_r*0.5, side * hip_r*0.7, side * hip_r*0.9, side * ankle*1.5, side * ankle]
105
- leg_y = [0, 0, 0, 0, 0]
106
- leg_z = [body_h, body_h*0.6, body_h*0.55, body_h*0.55 - (pant_len-crotch)*0.5, body_h*0.55 - pant_len*0.7]
107
- parts.append(dict(x=leg_x, y=leg_y, z=leg_z, type='scatter3d', mode='lines',
108
- line=dict(color='#2C5F8A', width=5), name='Leg R' if side > 0 else 'Leg L'))
109
- return parts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- def _garment_mesh_skirt(params, body_verts, n_theta, z_levels):
112
- parts = []
113
- waist_r = params.get('waist', 74)/2
114
- hip_r = params.get('hip', 96)/2
115
- skirt_len = params.get('skirt_length', 55)
116
- flare = params.get('flare', 5)
117
  body_h = 170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  waist_z = body_h * 0.58
119
- hem_z = waist_z - skirt_len
120
- hem_r = hip_r + flare
121
- theta = np.linspace(0, 2*np.pi, 40)
122
- for t_val, r, z_val, c, nm in [(0, waist_r, waist_z, '#D4A5C9', 'Waist'), (1, hip_r, waist_z - 20, '#C48BB8', 'Hip'), (2, hem_r, hem_z, '#E8C5E0', 'Hem')]:
123
- xs = [r * np.cos(th)*0.85 for th in theta]
124
- ys = [r * np.sin(th)*0.5 for th in theta]
125
- zs = [z_val]*len(theta)
126
- parts.append(dict(x=xs, y=ys, z=zs, type='scatter3d', mode='lines',
127
- line=dict(color=c, width=3), name=nm))
128
- return parts
129
 
130
- def _garment_mesh_dress(params, body_verts, n_theta, z_levels):
131
- parts = _garment_mesh_shirt(params, body_verts, n_theta, z_levels)
132
- parts += _garment_mesh_skirt(params, body_verts, n_theta, z_levels)
133
- return parts
134
-
135
- def _garment_mesh_hoodie(params, body_verts, n_theta, z_levels):
136
- parts = _garment_mesh_shirt(params, body_verts, n_theta, z_levels)
137
- head_circ = params.get('head_circumference', 57)/2
138
- body_len = params.get('bodice_length', 65)
 
 
 
 
 
 
 
 
 
139
  body_h = 170
140
- hood_z = [body_h*0.9, body_h*0.95, body_h*1.02, body_h*1.05, body_h*1.02, body_h*0.95]
141
- hood_x = [head_circ*0.8, head_circ, head_circ*0.7, 0, -head_circ*0.7, -head_circ]
142
- hood_y = [0, head_circ*0.3, head_circ*0.5, head_circ*0.6, head_circ*0.5, head_circ*0.3]
143
- parts.append(dict(x=hood_x, y=hood_y, z=hood_z, type='scatter3d', mode='lines',
144
- line=dict(color='#3A7BD5', width=5), name='Hood'))
145
- return parts
146
 
147
- def _garment_mesh_jacket(params, body_verts, n_theta, z_levels):
148
- return _garment_mesh_shirt(params, body_verts, n_theta, z_levels)
149
 
150
- def _garment_mesh_default(params, body_verts, n_theta, z_levels):
151
- return _garment_mesh_shirt(params, body_verts, n_theta, z_levels)
152
-
153
- GARMENT_MESH_FUNCS = {
154
- 'shirt': _garment_mesh_shirt,
155
- 'blouse': _garment_mesh_shirt,
156
- 'top': _garment_mesh_shirt,
157
- 't-shirt': _garment_mesh_shirt,
158
- 'tee': _garment_mesh_shirt,
159
- 'dress': _garment_mesh_dress,
160
- 'skirt': _garment_mesh_skirt,
161
- 'pants': _garment_mesh_pants,
162
- 'trousers': _garment_mesh_pants,
163
- 'jeans': _garment_mesh_pants,
164
- 'jacket': _garment_mesh_jacket,
165
- 'coat': _garment_mesh_jacket,
166
- 'blazer': _garment_mesh_jacket,
167
- 'hoodie': _garment_mesh_hoodie,
168
- 'sweatshirt': _garment_mesh_hoodie,
169
- 'vest': _garment_mesh_shirt,
170
  }
171
-
172
  def create_3d_figure(analysis: Dict) -> go.Figure:
173
- """Create interactive 3D garment visualization."""
174
- garment_type = analysis.get('garment_type', 'shirt').lower()
175
- measurements = analysis.get('measurements', {})
176
- features = analysis.get('features', {})
177
  params = {**measurements, **features}
178
- chest = params.get('bust', 92)
179
- waist_m = params.get('waist', 74)
180
- hip_m = params.get('hip', 96)
181
- body_v, body_f = create_body_mesh(chest=chest, waist=waist_m, hip=hip_m)
182
  fig = go.Figure()
183
- # Body wireframe
184
- n_t = 30
185
- for i in range(0, len(body_v), n_t*3):
186
- row = body_v[i:i+n_t]
187
- fig.add_trace(go.Scatter3d(
188
- x=row[:,0], y=row[:,1], z=row[:,2],
189
- mode='lines', line=dict(color='#DDCCAA', width=1),
190
- showlegend=False, hoverinfo='skip'))
191
- # Garment
192
- mesh_func = GARMENT_MESH_FUNCS.get(garment_type, _garment_mesh_default)
193
- garment_traces = mesh_func(params, body_v, n_t, np.linspace(0, 170, 40))
194
- for trace in garment_traces:
195
- fig.add_trace(go.Scatter3d(
196
- x=trace['x'], y=trace['y'], z=trace['z'],
197
- mode=trace.get('mode', 'lines'),
198
- line=trace.get('line', dict(width=3)),
199
- name=trace.get('name', '')))
 
 
200
  fig.update_layout(
201
  scene=dict(
202
- xaxis=dict(showgrid=False, showticklabels=False, title=''),
203
- yaxis=dict(showgrid=False, showticklabels=False, title=''),
204
- zaxis=dict(showgrid=False, showticklabels=False, title=''),
205
- aspectmode='data',
206
- camera=dict(eye=dict(x=1.5, y=1.5, z=0.8))),
207
  margin=dict(l=0, r=0, t=30, b=0),
208
  height=500,
209
- title=dict(text=f'3D Preview: {garment_type.title()}', font=dict(size=14)),
210
- paper_bgcolor='#fafafa',
211
- )
212
- return fig
 
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