Skip to main content

PyMeshUp

PyMeshUp provides an easy way to create and modify volume meshes using Python. It combines functionality from pymeshlab, VTK and CadQuery. Optional 3D visualization is available via vedo.

Geomtric data can be read from step files (via cadquery), stl, obj, etc (via pymeshlab) and GHS (custom python code).

The package is primarily aimed at ships and other floating structures.

A gui and compiled version is available separately via pymeshup-gui


Installation

pip install pymeshup

For 3D visualization:

pip install vedo

Quick-start example

from pymeshup import Frame, Hull, Box, Cylinder, Plot

# Define cross-sections
midship = Frame(0, 0,  1, 0,  1, 2,  0, 2).autocomplete()   # rectangular half-frame, mirrored
bow     = Frame(0, 1)                                         # single point → sharp bow

# Build a hull
hull = Hull(0, midship, 10, midship, 15, bow)

# Combine with a box to keep only the above-waterline part
above_water = hull.crop(zmin=0)

Plot([hull, above_water])

API Reference

Frame

A Frame is a 2-D cross-section (slice). Axes convention: X → right, Y → up.

Construction

from pymeshup import Frame

# From flat coordinate list  x1,y1, x2,y2, ...
f = Frame(0, 0,  1, 0,  1, 1,  0, 1)

# From separate x and y lists
f = Frame.from_xy([0, 1, 1, 0], [0, 0, 1, 1])

# Single point (used as a sharp tip)
tip = Frame(0, 1)

The polygon is automatically closed (first point appended to the end if needed).

Methods

Method Description
frame.autocomplete() Mirror the half-frame over the x = 0 centre-line and return the full closed frame.
frame.scaled(x=1, y=1) Return a scaled copy.
frame.copy() Return an identical copy.
frame.center() Return the (x, y) centroid.
frame.is_identical_to(other) Return True if both frames share all the same points.

Properties

Property Description
frame.x List of x-coordinates.
frame.y List of y-coordinates.
frame.xy Tuple of (x, y) pairs.
frame.n Number of points (including the closing duplicate).

Example

from pymeshup import Frame

# Half-frame: bottom-centre → bilge → side → deck
half = Frame(0, 0,  1, 0,  1.5, 0.5,  1.5, 2)

# Mirror to full frame
full = half.autocomplete()
print(full.xy)
# ((0,0),(1,0),(1.5,0.5),(1.5,2),(-1.5,2),(-1.5,0.5),(-1,0),(0,0))

Volume

A Volume is a triangulated 3-D mesh. All operations return a new Volume (non-destructive).

Boolean operations

from pymeshup import Box, Cylinder

box = Box(xmin=-1, xmax=1, ymin=-1, ymax=1, zmin=0, zmax=2)
cyl = Cylinder(height=2, radius=0.4)

combined    = box.add(cyl)          # union
hollow      = box.remove(cyl)       # difference – cylinder removed from box
common_part = box.inside_of(cyl)    # intersection

Transformations

vol = Box()

moved   = vol.move(x=1, y=0, z=0.5)          # translate
scaled  = vol.scale(x=2, y=1, z=1)            # non-uniform scale
rotated = vol.rotate(x=0, y=0, z=45)          # Euler angles in degrees
mirror  = vol.mirrorXZ()                       # mirror in the XZ plane (negate Y)

For anything the helpers above do not cover, apply a 4x4 transformation matrix directly. The matrix acts on the vertices as column vectors, so p' = matrix @ [x, y, z, 1]: the upper-left 3x3 block holds rotation/scale/shear and the last column holds the translation.

import numpy as np

matrix = np.eye(4)
matrix[:3, 3] = (1, 2, 3)                      # translation
matrix[0, 0] = 2                               # scale x by 2

transformed = vol.transform(matrix)            # any 4x4, also accepts nested lists
restored    = transformed.transform_inverse(matrix)   # undoes it again

transform_inverse applies the inverse of the given matrix, so vol.transform(m).transform_inverse(m) returns the original volume. Useful to move a volume into a local frame, work on it there, and put it back. A matrix that can not be inverted (a singular one, e.g. a scale of 0 along an axis) raises a ValueError.

Cropping / cutting

vol = Box(xmin=-2, xmax=2, ymin=-2, ymax=2, zmin=-2, zmax=2)

cropped       = vol.crop(zmin=0)               # keep z >= 0
submerged     = vol.cut_at_waterline()         # keep z <= 0
port_side     = vol.cut_at_xz()               # keep y <= 0

Mesh quality

vol = Hull(0, frame_a, 10, frame_b)

remeshed = vol.regrid(iterations=20, pct=1)        # isotropic remeshing
cleaned  = vol.merge_close_vertices(pct=1)          # weld near-coincident vertices
simple   = vol.simplify()                           # decimate (reduce face count)

Rebuilding a hull (reframe)

reframe turns a messy, non-watertight or non-manifold hull mesh into a clean, closed, port/starboard-symmetric hull. It slices the hull into horizontal waterlines, stitches and re-lofts them, and caps the flat of bottom and the flat of deck. Because it builds the hull from waterlines, features such as a bulbous bow or a stern bulb come out as one smooth, continuous surface.

from pymeshup import Load

bad_hull = Load("messy_scan.stl")     # gappy / non-manifold hull shell
clean    = bad_hull.reframe()         # watertight, symmetric, manifold

print(clean.volume, clean.bounds)

The hull must be oriented with X along its length and Z upward (the waterlines are horizontal X-Y slices). Symmetry is assumed: only the y >= 0 (starboard) half is sliced and the result is always mirrored back to a full hull. The flat of bottom is placed at the true keel (z = zmin) and the flat of deck at the true sheer (z = zmax). Running reframe on an already-clean hull returns essentially the same hull.

Signature: reframe(n_waterlines=80, points_per_waterline=140, margin=0.01, full_hull=None)

Argument Default Description
n_waterlines 80 Number of horizontal slices from keel to deck (vertical resolution).
points_per_waterline 140 Points each waterline is resampled to (resolution along the length).
margin 0.01 Fraction of the depth held back from the extremes when slicing — a cut exactly on the flat bottom / deck is coincident and returns nothing.
full_hull None Whether the input carries both sides. None auto-detects it (any vertex at y < 0); a full hull is first clipped to its y >= 0 half. Pass True / False to state it explicitly (e.g. for a port-side half-model that auto-detect would misread).

Persistence

vol.save("my_mesh.stl")      # save to STL (or any format pymeshlab supports)

Properties

Property Description
vol.vertices numpy array of vertex positions (N, 3).
vol.volume Signed volume (float).
vol.center Centre of mass [x, y, z].
vol.bounds (xmin, xmax, ymin, ymax, zmin, zmax).

Box

Creates a box-shaped volume.

from pymeshup import Box

# Unit box centred at the origin
unit_box = Box()

# Custom box
tank = Box(xmin=0, xmax=5, ymin=-1, ymax=1, zmin=-2, zmax=0)
print(tank.volume)   # ≈ 20.0

Signature: Box(xmin=-0.5, xmax=0.5, ymin=-0.5, ymax=0.5, zmin=-0.5, zmax=0.5)


Cylinder

Creates a vertical cylinder with its base at the origin.

from pymeshup import Cylinder

cyl = Cylinder(height=3, radius=0.5, resolution=36)
print(cyl.bounds)   # (xmin, xmax, ymin, ymax, 0, 3)

Signature: Cylinder(height=1, radius=1, resolution=36)

The radius is adjusted so that the discretised cylinder has the exact target volume.


Hull

Builds a hull mesh by lofting a sequence of Frame cross-sections along the X axis.

from pymeshup import Frame, Hull

stern   = Frame(0, 0,  1, 0,  1, 1).autocomplete()
midship = Frame(0, 0,  2, 0,  2, 2).autocomplete()
bow     = Frame(0, 2)    # sharp tip

# Hull(x0, frame0, x1, frame1, ..., xn, framen)
vessel = Hull(0, stern,  5, midship,  15, midship,  20, bow)
print(vessel.volume)

Load from CSV file:

vessel = Hull("my_hull_frames.csv")

CSV format (tab- or comma-separated):

x_pos, y1, y2, ...
      , z1, z2, ...
x_pos, y1, y2, ...
      , z1, z2, ...

Load

Loads an existing mesh file (STL, OBJ, PLY, …).

from pymeshup import Load

mesh = Load("path/to/model.stl")
print(mesh.volume)

Plot

Visualises one or more volumes interactively (requires vedo).

from pymeshup import Box, Cylinder, Plot

a = Box()
b = Cylinder().move(z=1)

Plot([a, b])    # pass a list for multiple volumes
Plot(a)         # or a single volume

Install vedo with pip install vedo.


STEP

Loads a STEP file via CadQuery and converts it to a Volume.

from pymeshup import STEP

stp = STEP("model.step", scale=0.001)   # scale: e.g. mm → m
vol = stp.to_volume(angular_tolerance=5, linear_tolerance=1)
print(vol.bounds)

STEP.to_volume arguments:

Argument Default Description
angular_tolerance 5 Angular tolerance for tessellation (degrees).
linear_tolerance 1 Linear tolerance for tessellation.
filename None If provided, the intermediate STL is saved to this path.

GHSgeo

Reads a GHS Geometry File (.GF) and makes individual parts and shapes available as Volume objects.

from pymeshup import GHSgeo

geo = GHSgeo("vessel.GF")

# Access a named part
hull_volume = geo["HULL"]          # equivalent to geo.get_volume("HULL")

# Access raw (un-autocompleted) shapes by name
raw_shape = geo.shapes_raw["s1"]
print(raw_shape.bounds)
Attribute Description
geo.parts Dictionary of parsed parts, each containing a "volume" key.
geo.shapes_raw Dictionary of shapes as read from the file.
geo.warnings List of warning messages encountered during parsing.

Polygon Triangulation (helpers)

PyMeshUp ships a polygon-triangulation sub-package used internally by Frame to cap hull ends and to handle concave cross-sections. You can also call these utilities directly.

triangulate_poly — automatic backend selection

from pymeshup.helpers.triangulate_non_convex import triangulate_poly

Triangulates any simple (non-self-intersecting) 3-D polygon, including concave shapes. It first tries the fast VTK triangulator; if that fails it falls back to the pure-Python ear-clipping implementation.

from pymeshup.helpers.triangulate_non_convex import triangulate_poly

# A rectangular cross-section at x = -60 (y-z plane)
vertices = [
    (-60,  0.0, 0.0),
    (-60, -2.5, 0.0),
    (-60, -2.5, 4.0),
    (-60,  0.0, 4.0),
    (-60,  2.5, 4.0),
    (-60,  2.5, 0.0),
    (-60,  0.0, 0.0),   # closing point (same as first)
]

verts, faces = triangulate_poly(vertices)
# verts  → list of (x, y, z) tuples  (same as input)
# faces  → list of (i, j, k) index triples
print(len(faces), "triangles produced")   # 4

Returns

Value Type Description
verts list[tuple] The original vertices (pass-through).
faces list[tuple[int,int,int]] Triangle index triples referencing verts.

triangulate_poly_py — pure-Python ear-clipping

from pymeshup.helpers.earcut_2d import triangulate_poly_py

The pure-Python fallback. Useful when VTK is unavailable or when the polygon is non-planar (the function projects onto the best-fit plane first).

from pymeshup.helpers.earcut_2d import triangulate_poly_py

# A non-planar concave polygon (the "skeg" example from the module docstring)
vertices = [
    (-60,  0.0, 0.0),
    (-60, -2.5, 0.0),
    (-60, -2.5, 4.0),
    (-60,  0.0, 4.0),
    (-61,  1.0, 5.0),   # one vertex is off-plane → triggers projection
    (-60,  2.5, 0.0),
    (-60,  0.0, 0.0),
]

verts, faces = triangulate_poly_py(vertices)

Low-level earcut helpers

These are used internally by triangulate_poly_py but can be imported independently if needed.

from pymeshup.helpers.earcut_2d import (
    is_clockwise,
    is_point_inside_triangle,
    triangulate_ear_clipping_2d,
    find_plane,
)
Function Signature Description
is_clockwise (polygon) Returns True when the 2-D vertex list is ordered clockwise.
is_point_inside_triangle (p, a, b, c) Returns True when point p lies inside triangle (a, b, c).
find_plane (vertices) Given 3-D vertices, returns (ux, uy) — two orthonormal in-plane axes — suitable for projecting the polygon to 2-D.
triangulate_ear_clipping_2d (polygon) Ear-clipping triangulation of a 2-D polygon; returns a list of (i, j, k) index triples.
from pymeshup.helpers.earcut_2d import is_clockwise, triangulate_ear_clipping_2d

square = [(0, 0), (1, 0), (1, 1), (0, 1)]
print(is_clockwise(square))                  # False (counter-clockwise)

triangles = triangulate_ear_clipping_2d(square)
print(triangles)                              # [(0, 1, 2), (2, 3, 0)]  or similar

Full worked example

from pymeshup import Frame, Hull, Box, Cylinder, Plot

# --- 1. Build a simple hull ---
half_frame = Frame(0, 0,   3, 0,   3.5, 1,   3.5, 4)
midship    = half_frame.autocomplete()
bow        = Frame(0, 4)        # sharp bow at deck height

hull = Hull(0, midship,  8, midship,  12, bow)

# --- 2. Add a cylindrical funnel on top ---
funnel = Cylinder(height=2, radius=0.4).move(x=4, z=4)

# --- 3. Hollow out an engine-room box ---
engine_room = Box(xmin=1, xmax=5, ymin=-1.5, ymax=1.5, zmin=0, zmax=3)
hull_with_room = hull.remove(engine_room)

# --- 4. Combine ---
ship = hull_with_room.add(funnel)

print(f"Ship volume : {ship.volume:.1f} m³")
print(f"Bounding box: {ship.bounds}")

# Plot(ship)   # uncomment if vedo is installed

Metadata

Release files for pymeshup 26.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pymeshup 26.7.0
File Size Uploaded
pymeshup-26.7.0.tar.gz 4.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for pymeshup 26.7.0
File Interpreter ABI Platform
pymeshup-26.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 4.1 MB

Release files / pymeshup-26.7.0.tar.gz

Download URL pymeshup-26.7.0.tar.gz
Size 4.1 MB
Tags Source
SHA-256 checksum
How to use checksums
ff7c1d97795f6fa819664973153cb32f61cd114e6da59423c9a6b476091d50a5
BLAKE2b-256 checksum
How to use checksums
7f95cec030f297b4d1eee3ddbccb241d8106e202e8f5f1068a8a9e3fa4c1e2b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release files / pymeshup-26.7.0-py3-none-any.whl

Download URL pymeshup-26.7.0-py3-none-any.whl
Size 44.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba328f769237b5e0b6d13cf7c3f008bcbad344e9f3a7aa6b26218480b2d52f4e
BLAKE2b-256 checksum
How to use checksums
718ee32025610d4e317c57a716d78deeb3cd0c52a71a7f0ea9c0ba535f05f03d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

26.7.0 This release

2 release files

26.3.4

2 release files

26.3.3

2 release files

26.3.2

2 release files

25.5.3

2 release files

25.3.1

2 release files

25.2.1

2 release files

0.3.2

2 release files

0.3

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page