""" 3D Garment Visualizer — Built FROM 2D Pattern Pieces Takes the actual 2D sewing pattern pieces from pattern_generator.py, triangulates them, and wraps each piece onto the correct body region using cylindrical/cone projections. Each 3D surface directly corresponds to a 2D pattern piece. """ import numpy as np import plotly.graph_objects as go from typing import Dict, List, Tuple, Optional # ── Body constants (cm, Z=0 at floor, Z=170 at head top) ─────────────────── BODY_HEIGHT = 170.0 Z_ANKLE = 7 Z_KNEE = 45 Z_CROTCH = 82 Z_HIP = 88 Z_WAIST = 104 Z_BUST = 120 Z_SHOULDER = 145 Z_NECK = 150 Z_HEAD = 170 _BODY_Z = [0, 7, 30, 45, 65, 82, 88, 100, 104, 112, 120, 132, 140, 145, 150, 158, 162, 170] _BODY_RX = [4, 3, 4.5, 4, 6.5, 8, 9.5, 8, 7, 8.5, 10, 9, 8.5, 11.5, 3.5, 3, 5.5, 4.5] _BODY_RY = [3, 2.5, 4, 4, 6, 7.5, 8.5, 7.5, 6.5, 8, 9, 8, 7.5, 8, 3.5, 3, 5, 4.5] R_NECK = 5.5 R_WRIST = 3.5 PIECE_COLORS = { 'Front Bodice': '#5B9BD5', 'Back Bodice': '#4472C4', 'Sleeve': '#6BB3E0', 'Collar': '#FFFFFF', 'Cuff': '#E8E8E8', 'Front Skirt': '#C48BB8', 'Back Skirt': '#A86CA8', 'Front Pant': '#2C5F8A', 'Back Pant': '#1A4570', 'Waistband': '#333333', 'Hood': '#3A7BD5', 'Pocket': '#7A7A7A', } def _point_in_polygon(x, y, polygon): n = len(polygon) inside = False j = n - 1 for i in range(n): xi, yi = polygon[i] xj, yj = polygon[j] if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi + 1e-12) + xi): inside = not inside j = i return inside def _triangulate_piece(pts_2d, nu=16, nv=16): pts = np.array(pts_2d, dtype=float) if len(pts) < 3: return pts, np.zeros((0, 3), dtype=int) if np.allclose(pts[0], pts[-1]): pts = pts[:-1] x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() dx = max(x_max - x_min, 0.1) dy = max(y_max - y_min, 0.1) us = np.linspace(x_min - dx * 0.01, x_max + dx * 0.01, nu) vs = np.linspace(y_min - dy * 0.01, y_max + dy * 0.01, nv) grid_idx = -np.ones((nv, nu), dtype=int) valid_pts = [] for i in range(nv): for j in range(nu): if _point_in_polygon(us[j], vs[i], pts): grid_idx[i, j] = len(valid_pts) valid_pts.append([us[j], vs[i]]) if len(valid_pts) < 3: return _triangulate_fan(pts) tris = [] for i in range(nv - 1): for j in range(nu - 1): a, b = grid_idx[i, j], grid_idx[i, j + 1] c, d = grid_idx[i + 1, j], grid_idx[i + 1, j + 1] if a >= 0 and b >= 0 and c >= 0: tris.append([a, b, c]) if b >= 0 and c >= 0 and d >= 0: tris.append([b, d, c]) if len(tris) == 0: return _triangulate_fan(pts) return np.array(valid_pts), np.array(tris) def _triangulate_fan(pts): n = len(pts) if n < 3: return pts, np.zeros((0, 3), dtype=int) centroid = pts.mean(axis=0, keepdims=True) all_pts = np.vstack([pts, centroid]) tris = np.array([[i, (i + 1) % n, n] for i in range(n)]) return all_pts, tris def _wrap_cylinder(pts_2d, radius, z_bottom, z_top, angle_start, angle_range, x_offset=0.0, y_offset=0.0): pts = np.array(pts_2d, dtype=float) x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() x_norm = (pts[:, 0] - x_min) / max(x_max - x_min, 0.01) y_norm = (pts[:, 1] - y_min) / max(y_max - y_min, 0.01) theta = angle_start + x_norm * angle_range z = z_bottom + y_norm * (z_top - z_bottom) x_3d = radius * np.cos(theta) + x_offset y_3d = radius * np.sin(theta) + y_offset return np.stack([x_3d, y_3d, z], axis=1) def _wrap_cone(pts_2d, r_top, r_bottom, z_top, z_bottom, angle_start, angle_range): pts = np.array(pts_2d, dtype=float) x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() x_norm = (pts[:, 0] - x_min) / max(x_max - x_min, 0.01) y_norm = (pts[:, 1] - y_min) / max(y_max - y_min, 0.01) theta = angle_start + x_norm * angle_range z = z_top - y_norm * (z_top - z_bottom) radius = r_top + y_norm * (r_bottom - r_top) x_3d = radius * np.cos(theta) y_3d = radius * np.sin(theta) return np.stack([x_3d, y_3d, z], axis=1) def _wrap_sleeve(pts_2d, side='right', shoulder_x=14.0, shoulder_z=142.0, arm_length=35.0, arm_angle_deg=25.0, r_top=5.5, r_bottom=3.5): pts = np.array(pts_2d, dtype=float) x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() x_norm = (pts[:, 0] - x_min) / max(x_max - x_min, 0.01) y_norm = (pts[:, 1] - y_min) / max(y_max - y_min, 0.01) theta = x_norm * 2 * np.pi t_arm = 1.0 - y_norm radius = r_top + t_arm * (r_bottom - r_top) lx = radius * np.cos(theta) ly = -t_arm * arm_length lz = radius * np.sin(theta) ang = np.radians(arm_angle_deg) rx = lx * np.cos(ang) - ly * np.sin(ang) rz_local = lx * np.sin(ang) + ly * np.cos(ang) sign = 1.0 if side == 'right' else -1.0 return np.stack([sign * (rx + shoulder_x), lz, rz_local + shoulder_z], axis=1) def _wrap_pant_leg(pts_2d, side='right', hip_x=5.0, z_top=88.0, z_bottom=7.0, r_top=8.5, r_bottom=4.5, is_front=True): pts = np.array(pts_2d, dtype=float) x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() x_norm = (pts[:, 0] - x_min) / max(x_max - x_min, 0.01) y_norm = (pts[:, 1] - y_min) / max(y_max - y_min, 0.01) if is_front: theta = -np.pi / 2 + x_norm * np.pi else: theta = np.pi / 2 + x_norm * np.pi z = z_bottom + y_norm * (z_top - z_bottom) radius = r_bottom + y_norm * (r_top - r_bottom) sign = 1.0 if side == 'right' else -1.0 x_3d = sign * hip_x + radius * np.cos(theta) y_3d = radius * np.sin(theta) return np.stack([x_3d, y_3d, z], axis=1) def _wrap_hood(pts_2d, z_base=145.0, z_top=172.0, r_base=12.0, r_top=7.0): pts = np.array(pts_2d, dtype=float) x_min, x_max = pts[:, 0].min(), pts[:, 0].max() y_min, y_max = pts[:, 1].min(), pts[:, 1].max() x_norm = (pts[:, 0] - x_min) / max(x_max - x_min, 0.01) y_norm = (pts[:, 1] - y_min) / max(y_max - y_min, 0.01) theta = x_norm * np.pi z = z_base + y_norm * (z_top - z_base) radius = r_base + y_norm * (r_top - r_base) radius *= (1.0 - 0.3 * y_norm ** 2) x_3d = radius * np.cos(theta) y_3d = radius * np.sin(theta) - 3 return np.stack([x_3d, y_3d, z], axis=1) def _match_piece_type(name): name_lower = name.lower() if 'front bodice' in name_lower: return 'front_bodice' if 'back bodice' in name_lower: return 'back_bodice' if 'sleeve' in name_lower: return 'sleeve' if 'collar' in name_lower: return 'collar' if 'cuff' in name_lower: return 'cuff' if 'front skirt' in name_lower: return 'front_skirt' if 'back skirt' in name_lower: return 'back_skirt' if 'front pant' in name_lower: return 'front_pant' if 'back pant' in name_lower: return 'back_pant' if 'waistband' in name_lower: return 'waistband' if 'hood' in name_lower: return 'hood' if 'pocket' in name_lower: return 'pocket' return 'unknown' def _get_piece_color(name): for key, color in PIECE_COLORS.items(): if key.lower() in name.lower(): return color return '#888888' def _make_body_trace(): theta = np.linspace(0, 2 * np.pi, 48) z_arr = np.array(_BODY_Z, float) rx = np.array(_BODY_RX, float) ry = np.array(_BODY_RY, float) Z = np.outer(z_arr, np.ones(48)) X = np.outer(rx, np.cos(theta)) Y = np.outer(ry, np.sin(theta)) return go.Surface( x=X, y=Y, z=Z, surfacecolor=np.ones_like(X) * 0.8, colorscale=[[0, "#E8D0B0"], [1, "#E8D0B0"]], opacity=0.20, showscale=False, name="Body", hoverinfo="skip", lighting=dict(ambient=0.9, diffuse=0.1, specular=0, roughness=1.0), ) def create_3d_figure(analysis: Dict, pattern_pieces: Optional[List[Dict]] = None) -> go.Figure: garment_type = analysis.get("garment_type", "shirt").lower() measurements = analysis.get("measurements", {}) features = analysis.get("features", {}) fig = go.Figure() fig.add_trace(_make_body_trace()) if pattern_pieces is None or len(pattern_pieces) == 0: _setup_layout(fig, garment_type) return fig for piece in pattern_pieces: name = piece['name'] pts_2d = piece['points'] piece_type = _match_piece_type(name) color = _get_piece_color(name) mesh_pts, tris = _triangulate_piece(pts_2d) if len(tris) == 0: continue traces = _wrap_piece(piece_type, mesh_pts, tris, name, color, measurements, features, garment_type) for trace in traces: fig.add_trace(trace) _setup_layout(fig, garment_type) return fig def _wrap_piece(piece_type, mesh_pts, tris, name, color, measurements, features, garment_type): traces = [] bust = measurements.get('bust', 92) waist = measurements.get('waist', 74) hip = measurements.get('hip', 96) sleeve_len = measurements.get('sleeve_length', 60) skirt_len = measurements.get('skirt_length', 55) pant_len = measurements.get('pant_length', 100) r_torso = bust / (2 * np.pi) + 1.5 r_waist = waist / (2 * np.pi) + 1.5 r_hip = hip / (2 * np.pi) + 1.5 if piece_type == 'front_bodice': pts_3d = _wrap_cylinder(mesh_pts, r_torso, Z_WAIST, Z_SHOULDER, -np.pi / 2, np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'back_bodice': pts_3d = _wrap_cylinder(mesh_pts, r_torso, Z_WAIST, Z_SHOULDER, np.pi / 2, np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'sleeve': shoulder_x = r_torso + 1.0 for side in ['right', 'left']: pts_3d = _wrap_sleeve(mesh_pts, side=side, shoulder_x=shoulder_x, shoulder_z=Z_SHOULDER - 2, arm_length=sleeve_len * 0.55, arm_angle_deg=25, r_top=5.5, r_bottom=3.5) traces.append(_make_mesh3d(pts_3d, tris, color, f"{name} ({'R' if side == 'right' else 'L'})")) elif piece_type == 'collar': pts_3d = _wrap_cylinder(mesh_pts, R_NECK + 1.0, Z_NECK - 2, Z_NECK + 3, -np.pi, 2 * np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'cuff': for side in ['right', 'left']: sign = 1.0 if side == 'right' else -1.0 wrist_x = sign * (r_torso + sleeve_len * 0.3 + 2) wrist_z = Z_SHOULDER - sleeve_len * 0.5 pts_3d = _wrap_cylinder(mesh_pts, R_WRIST + 0.5, wrist_z - 3, wrist_z, 0, 2 * np.pi, x_offset=wrist_x) traces.append(_make_mesh3d(pts_3d, tris, color, f"{name} ({'R' if side == 'right' else 'L'})")) elif piece_type == 'front_skirt': hem_z = max(Z_WAIST - skirt_len, Z_ANKLE) flare = measurements.get('flare', 5) r_hem = r_hip + flare * 0.5 pts_3d = _wrap_cone(mesh_pts, r_waist, r_hem, Z_WAIST, hem_z, -np.pi / 2, np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'back_skirt': hem_z = max(Z_WAIST - skirt_len, Z_ANKLE) flare = measurements.get('flare', 5) r_hem = r_hip + flare * 0.5 pts_3d = _wrap_cone(mesh_pts, r_waist, r_hem, Z_WAIST, hem_z, np.pi / 2, np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'front_pant': hem_z = max(Z_HIP - pant_len * 0.85, Z_ANKLE) thigh_circ = measurements.get('thigh', 56) ankle_circ = measurements.get('ankle', 24) r_thigh = thigh_circ / (2 * np.pi) + 1 r_ankle_r = ankle_circ / (2 * np.pi) + 1 for side in ['right', 'left']: pts_3d = _wrap_pant_leg(mesh_pts, side=side, hip_x=r_hip * 0.4, z_top=Z_HIP, z_bottom=hem_z, r_top=r_thigh, r_bottom=r_ankle_r, is_front=True) traces.append(_make_mesh3d(pts_3d, tris, color, f"{name} ({'R' if side == 'right' else 'L'})")) elif piece_type == 'back_pant': hem_z = max(Z_HIP - pant_len * 0.85, Z_ANKLE) thigh_circ = measurements.get('thigh', 56) ankle_circ = measurements.get('ankle', 24) r_thigh = thigh_circ / (2 * np.pi) + 1 r_ankle_r = ankle_circ / (2 * np.pi) + 1 for side in ['right', 'left']: pts_3d = _wrap_pant_leg(mesh_pts, side=side, hip_x=r_hip * 0.4, z_top=Z_HIP, z_bottom=hem_z, r_top=r_thigh, r_bottom=r_ankle_r, is_front=False) traces.append(_make_mesh3d(pts_3d, tris, color, f"{name} ({'R' if side == 'right' else 'L'})")) elif piece_type == 'waistband': wb_h = measurements.get('waistband_height', 4) pts_3d = _wrap_cylinder(mesh_pts, r_waist + 0.5, Z_WAIST, Z_WAIST + wb_h, -np.pi, 2 * np.pi) traces.append(_make_mesh3d(pts_3d, tris, color, name)) elif piece_type == 'hood': pts_3d_r = _wrap_hood(mesh_pts, z_base=Z_SHOULDER, z_top=Z_HEAD + 2, r_base=r_torso, r_top=7) traces.append(_make_mesh3d(pts_3d_r, tris, color, f"{name} (R)")) pts_3d_l = pts_3d_r.copy() pts_3d_l[:, 0] = -pts_3d_l[:, 0] traces.append(_make_mesh3d(pts_3d_l, tris, color, f"{name} (L)")) elif piece_type == 'pocket': pocket_z = (Z_WAIST + Z_BUST) / 2 pts_3d = _wrap_cylinder(mesh_pts, r_torso + 0.3, pocket_z - 8, pocket_z, -0.3, 0.6) traces.append(_make_mesh3d(pts_3d, tris, color, name)) return traces def _make_mesh3d(pts_3d, tris, color, name): return go.Mesh3d( x=pts_3d[:, 0], y=pts_3d[:, 1], z=pts_3d[:, 2], i=tris[:, 0], j=tris[:, 1], k=tris[:, 2], color=color, opacity=0.85, name=name, flatshading=False, lighting=dict(ambient=0.55, diffuse=0.8, specular=0.2, roughness=0.6), lightposition=dict(x=100, y=200, z=300), hoverinfo="name", ) def _setup_layout(fig, garment_type): fig.update_layout( scene=dict( xaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False, range=[-40, 40]), yaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False, range=[-40, 40]), zaxis=dict(showgrid=False, showticklabels=False, title="", zeroline=False, range=[0, 180]), aspectmode="data", camera=dict(eye=dict(x=1.8, y=1.2, z=0.5), center=dict(x=0, y=0, z=-0.1)), bgcolor="rgba(245,245,248,1)", ), margin=dict(l=0, r=0, t=35, b=0), height=550, title=dict(text=f"3D Preview: {garment_type.title()} (from pattern pieces)", font=dict(size=14)), paper_bgcolor="#fafafa", showlegend=True, legend=dict(font=dict(size=10), bgcolor="rgba(255,255,255,0.8)"), )