##  [Importing Grouped OBJ Files into PyPrimeMesh](/blog/importing-grouped-obj-files-pyprimemesh) 

PyPrimeMesh imports geometry through its CAD reader, which does not natively read OBJ files. When an OBJ file uses `g` group records to divide geometry into named zones — such as inlet, outlet, and wall surfaces — those groups correspond to Named Selections in simulation pre-processing tools. That zone identity, along with the Named Selection it represents, would normally be lost during any intermediate format conversion.

This article describes a pre-processing workflow that bridges that gap. A Python converter reads a single grouped OBJ file and splits it into one binary STL file per group, naming each file after its group. PyPrimeMesh then imports those STL files through its standard CAD route, one part per file. Part names are applied as labels on the imported face zonelets, and zones are created from those labels, so the original OBJ group names — and the Named Selections they represent — are fully traceable through to the final mesh.

The workflow requires no manual geometry editing and no CAD software. The only inputs are the grouped OBJ file and a Python environment with `numpy`, `pyvista`, and `ansys-meshing-prime` installed.

The workflow preserves grouped OBJ geometry for PyPrimeMesh by splitting one OBJ file into one STL file per OBJ group. This is useful when an OBJ contains multiple named zones that need to remain identifiable after import, labeling, wrapping, and meshing.

The key idea is to keep the group name attached to the geometry as long as possible: first as an OBJ `g` record, then as an STL file name and header, then as a Prime part name, and finally as a label-derived zone.

## Group-Preservation Logic

The conversion logic reads OBJ vertices and group records, stores faces under each group name, triangulates polygon faces, and writes one binary STL per group. The parser keeps one shared vertex list for the full OBJ file, while each group gets its own face list. This keeps geometry compact during parsing and allows each group to be exported independently afterward.

The important parsing rules are:

- Lines beginning with `v` are read as XYZ vertex coordinates and stored in input order.
- Lines beginning with `g` define the active OBJ group. That group name becomes the zone identity used downstream.
- Lines beginning with `f` are stored under the currently active group.
- Face tokens such as `12`, `12/4`, or `12/4/9` are supported by reading only the vertex index before the first `/`.
- OBJ indices are converted from one-based indexing to Python zero-based indexing.
- Negative OBJ indices are resolved relative to the current vertex list, which matches the OBJ convention for referencing recently defined vertices.
- Polygonal faces are triangulated so the STL output contains only triangles.

```python
def convert_obj_to_stl(input_path: Path) -> list[Path]:
    vertices, faces_by_zone = parse_obj_zones(input_path)
    output_files: list[Path] = []

    for zone_name, faces in faces_by_zone.items():
        if not faces:
            continue

        output_files.append(write_zone_stl(input_path, zone_name, faces, vertices))

    return output_files

```

OBJ `g` lines are treated as zone names. When a group is encountered, subsequent faces are collected under that group until another group is found.

```python
elif tag == 'g':
    zone_name = ' '.join(parts[1:]).strip()
    if zone_name:
        current_zone = zone_name
        faces_by_zone.setdefault(current_zone, [])

```

Face records can include vertex, texture, and normal references. Only the vertex reference is needed for the STL surface, so each face token is split at `/` and the first field is used.

```python
for token in parts[1:]:
    vertex_token = token.split('/')[0]
    if not vertex_token:
        continue
    vertex_index = int(vertex_token)
    if vertex_index < 0:
        vertex_index = len(vertices) + vertex_index + 1
    vertex_indices.append(vertex_index - 1)

```

```python
first_vertex = vertex_indices[0]
for index in range(1, len(vertex_indices) - 1):
    zone_faces.append((first_vertex, vertex_indices[index], vertex_indices[index + 1]))

```

Faces with more than three vertices are converted to triangles using a simple fan triangulation.

Each group is then converted to a PyVista `PolyData` surface and exported as STL.

> **Important:** The output STL file name is derived from the OBJ group name after removing characters that are unsafe for file systems. This is what allows the original group identity to become visible again during downstream PyPrimeMesh import.

```python
def write_zone_stl(input_path: Path, zone_name: str, faces: list[tuple[int, int, int]], vertices: list[list[float]]) -> Path:
    output_name = sanitize_name(zone_name)
    output_path = input_path.with_name(f'{output_name}.stl')
    polydata = build_polydata(vertices, faces)
    polydata.save(str(output_path))
    rename_binary_stl_header(output_path, output_name)
    return output_path

```

## Downstream PyPrimeMesh Usage

Install or enable the required Python packages in the same environment used by PyPrimeMesh:

```powershell
pip install numpy pyvista ansys-meshing-prime

```

Run the OBJ-to-STL split on the grouped OBJ file before calling Prime CAD import.

```python
from pathlib import Path

import ansys.meshing.prime as prime

from convertor import convert_obj_to_stl

prime_client = prime.launch_prime()
model = prime_client.model
mesh_util = prime.lucid.Mesh(model)

template_geom_file = Path('mechanical_sphere_w_groups.obj')
stl_files = convert_obj_to_stl(template_geom_file)
if not stl_files:
    raise RuntimeError(f'No STL files were generated from {template_geom_file}')

```

Then import each STL through the CAD route. The `append=True` setting allows all generated STL files to be brought into the same model.

```python
file_io = prime.FileIO(model=model)
cad_params = prime.ImportCadParams(
    model,
    part_creation_type=prime.PartCreationType.PART,
    cad_reader_route=prime.CadReaderRoute.PROGRAMCONTROLLED,
    append=True,
)

for stl_file in stl_files:
    file_io.import_cad(file_name=str(stl_file), params=cad_params)
    print(f'imported {stl_file}')

```

After import, each Prime part name is applied as a label on its face zonelets, and zones are created from those labels. This carries the original OBJ group identity into the Prime model for later wrapping and meshing operations.

```python
for part in model.parts:
    part.add_labels_on_zonelets(labels=[part.name], zonelets=part.get_face_zonelets())

mesh_util.create_zones_from_labels()

```

## End-to-End Workflow

- Prepare an OBJ file with meaningful `g` group records.
- Run `convert_obj_to_stl(Path('your_grouped_file.obj'))`.
- Import each generated STL with `prime.FileIO.import_cad(..., append=True)`.
- Add labels from part names and create zones from labels.
- Continue with sizing controls, surface wrapping, volume meshing, and mesh export.