def _get_solid_objects(model_data): """Safely extracts unique 3D concrete solid meshes from model_data['objects'].""" objs = [] seen = set() raw = model_data.get('objects', {}) if 'culvert' in raw and isinstance(raw['culvert'], dict): c = raw['culvert'] objs.append(c) seen.add(c.get('id', 'CULVERT')) if 'wingwalls' in raw and isinstance(raw['wingwalls'], list): for w in raw['wingwalls']: if isinstance(w, dict) and w.get('id') not in seen and 'vertices' in w: objs.append(w) seen.add(w['id']) else: for k, v in raw.items(): if isinstance(v, dict) and v.get('id') and v['id'] not in seen and 'vertices' in v: objs.append(v) seen.add(v['id']) return objs def export_obj(model_data, include_rebars=True): """ Exports 3D model data (exterior concrete solids + 3D rebars) to Wavefront OBJ format. """ lines = ["# Wingwall & Culvert 3D Solid & Rebar Model", "# Generated by Antigravity CAD/BIM Modeler", ""] v_offset = 1 solid_objects = _get_solid_objects(model_data) for obj in solid_objects: lines.append(f"o {obj['id']}") lines.append(f"g {obj['name']}") for v in obj['vertices']: lines.append(f"v {v[0]:.4f} {v[1]:.4f} {v[2]:.4f}") for f in obj['faces']: lines.append(f"f {f[0] + v_offset} {f[1] + v_offset} {f[2] + v_offset}") v_offset += len(obj['vertices']) lines.append("") if include_rebars and 'rebars' in model_data: for ww_id, ww_rebar in model_data['rebars'].get('wingwalls', {}).items(): for grp in ww_rebar.get('groups', []): lines.append(f"o REBAR_{ww_id}_{grp['mark']}_{grp['dia']}") for poly in grp.get('polylines', []): if len(poly) < 2: continue for pt in poly: lines.append(f"v {pt[0]:.4f} {pt[1]:.4f} {pt[2]:.4f}") idx_str = " ".join(str(v_offset + i) for i in range(len(poly))) lines.append(f"l {idx_str}") v_offset += len(poly) lines.append("") return "\n".join(lines) def export_stl(model_data): """ Exports 3D model data to ASCII STL format. """ lines = ["solid WingwallCulvertModel"] for obj in _get_solid_objects(model_data): v = obj['vertices'] for f in obj['faces']: p1 = v[f[0]] p2 = v[f[1]] p3 = v[f[2]] v1 = [p2[0]-p1[0], p2[1]-p1[1], p2[2]-p1[2]] v2 = [p3[0]-p1[0], p3[1]-p1[1], p3[2]-p1[2]] nx = v1[1]*v2[2] - v1[2]*v2[1] ny = v1[2]*v2[0] - v1[0]*v2[2] nz = v1[0]*v2[1] - v1[1]*v2[0] mag = (nx*nx + ny*ny + nz*nz)**0.5 if mag > 1e-8: nx, ny, nz = nx/mag, ny/mag, nz/mag else: nx, ny, nz = 0.0, 0.0, 1.0 lines.append(f" facet normal {nx:.4e} {ny:.4e} {nz:.4e}") lines.append(" outer loop") lines.append(f" vertex {p1[0]:.4f} {p1[1]:.4f} {p1[2]:.4f}") lines.append(f" vertex {p2[0]:.4f} {p2[1]:.4f} {p2[2]:.4f}") lines.append(f" vertex {p3[0]:.4f} {p3[1]:.4f} {p3[2]:.4f}") lines.append(" endloop") lines.append(" endfacet") lines.append("endsolid WingwallCulvertModel") return "\n".join(lines) def export_dxf_3d(model_data, include_rebars=True): """ Exports 3D model data to DXF R2000 ASCII format containing 3DFACE entities for concrete and 3D LINE entities for internal rebars. """ lines = [ "0", "SECTION", "2", "ENTITIES" ] for obj in _get_solid_objects(model_data): layer = obj['id'] v = obj['vertices'] for f in obj['faces']: p1 = v[f[0]] p2 = v[f[1]] p3 = v[f[2]] lines.extend([ "0", "3DFACE", "8", layer, "10", f"{p1[0]:.4f}", "20", f"{p1[1]:.4f}", "30", f"{p1[2]:.4f}", "11", f"{p2[0]:.4f}", "21", f"{p2[1]:.4f}", "31", f"{p2[2]:.4f}", "12", f"{p3[0]:.4f}", "22", f"{p3[1]:.4f}", "32", f"{p3[2]:.4f}", "13", f"{p3[0]:.4f}", "23", f"{p3[1]:.4f}", "33", f"{p3[2]:.4f}" ]) if include_rebars and 'rebars' in model_data: color_by_dia = {'H19': '1', 'H16': '4', 'H13': '3'} for ww_id, ww_rebar in model_data['rebars'].get('wingwalls', {}).items(): for grp in ww_rebar.get('groups', []): layer = f"REBAR_{ww_id}_{grp['mark']}_{grp['dia']}" c_code = color_by_dia.get(grp['dia'], '2') for poly in grp.get('polylines', []): for i in range(len(poly) - 1): p1 = poly[i] p2 = poly[i + 1] lines.extend([ "0", "LINE", "8", layer, "62", c_code, "10", f"{p1[0]:.4f}", "20", f"{p1[1]:.4f}", "30", f"{p1[2]:.4f}", "11", f"{p2[0]:.4f}", "21", f"{p2[1]:.4f}", "31", f"{p2[2]:.4f}" ]) lines.extend([ "0", "ENDSEC", "0", "EOF" ]) return "\n".join(lines) def export_rebar_bom_csv(model_data): """ Exports 3단계 철근 재료표 (Bar Bending Schedule) to UTF-8 BOM CSV format. """ rows = [ "\ufeff구분(날개벽),기호(Mark),직경(Dia),배치부위,역할,간격,절곡치수,1본길이(m),수량(개),총길이(m),단위중량(kg/m),총중량(TON)" ] if 'rebars' in model_data: for ww_id, ww in model_data['rebars'].get('wingwalls', {}).items(): ww_name = ww.get('wingwall_name', ww_id) for g in ww.get('groups', []): shape_str = " / ".join(f"{k}={v}" for k, v in g.get('shape_params', {}).items()) rows.append( f'"{ww_name}",{g["mark"]},{g["dia"]},"{g["zone"]}","{g["role"]}","{g["spacing"]}",' f'"{shape_str}",{g["length_m"]:.3f},{g["count"]},{g["total_length_m"]:.3f},' f'{g["unit_weight"]:.3f},{g["weight_ton"]:.4f}' ) s = ww.get('summary', {}) rows.append( f'"{ww_name} 소계",-,전체,-,-,-,-,-,{s.get("total_bars",0)},' f'{s.get("total_length_m",0):.3f},-,{s.get("total_weight_ton",0):.3f} (할증3%: {s.get("total_surcharge_ton",0):.3f})' ) return "\n".join(rows)