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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymeshup-26.7.0.tar.gz (4.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymeshup-26.7.0-py3-none-any.whl (44.9 kB view details)

Uploaded Python 3

File details

Details for the file pymeshup-26.7.0.tar.gz.

File metadata

  • Download URL: pymeshup-26.7.0.tar.gz
  • Upload date:
  • Size: 4.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pymeshup-26.7.0.tar.gz
Algorithm Hash digest
SHA256 ff7c1d97795f6fa819664973153cb32f61cd114e6da59423c9a6b476091d50a5
MD5 c107a794ffd40be678441bd2dd7a6f47
BLAKE2b-256 7f95cec030f297b4d1eee3ddbccb241d8106e202e8f5f1068a8a9e3fa4c1e2b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymeshup-26.7.0.tar.gz:

Publisher: python-publish.yml on RubendeBruin/pymeshup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymeshup-26.7.0-py3-none-any.whl.

File metadata

  • Download URL: pymeshup-26.7.0-py3-none-any.whl
  • Upload date:
  • Size: 44.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pymeshup-26.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ba328f769237b5e0b6d13cf7c3f008bcbad344e9f3a7aa6b26218480b2d52f4e
MD5 a710cbe60cf134096924c7e9d573f375
BLAKE2b-256 718ee32025610d4e317c57a716d78deeb3cd0c52a71a7f0ea9c0ba535f05f03d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymeshup-26.7.0-py3-none-any.whl:

Publisher: python-publish.yml on RubendeBruin/pymeshup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page