Skip to content

visualdynamics.core.hexmesh

hexmesh

Brick meshing of any solid, over a grid fitted to its shape.

Mesh Solids meshes an extrusion into bricks on a grid with rings about its holes (solids.extrusion_mesh). A part that is not an extrusion is meshed here: a grid of cubes over it, the cubes whose centers are inside kept, and the stair-stepped skin that leaves fitted to the true surface. Nothing about the shape is assumed beyond its signed distance (solids.distance) and the sharp edges read from its recipe (tetmesh.sharp_edges).

The fitting is done in an order that keeps every brick the right way out. The skin is pillowed first: every surface node gets a twin a little way in, so each brick under the surface touches it by one face only, and a layer of new bricks lies between. The corners and sharp edges are captured by the nearest skin nodes, which are moved onto them and held; the skin between is smoothed so it runs nearly parallel to the surface it is about to meet, and then every skin node is projected onto the surface along the distance's gradient, in two stages, the inside smoothed after each. Last, every node takes the position among a few candidates that raises the worst scaled Jacobian of the bricks around it, surface nodes sliding on the surface, until no node moves.

The result has round holes and sharp edges whatever the shape, with bricks of lower quality than a ring mesh gives: the worst scaled Jacobian is typically between 0.15 and 0.3, and the pane reads it. Where the parts are an extrusion, extrusion_mesh is the better mesh and is used instead (brick_mesh).

Functions:

Name Description
overlay_mesh

The parts as eight-node bricks fitted to their shape, ready to

suggested_size

An element size at which a mesh of the parts is likely to hold:

likely_to_fold

Whether a fit at size is likely to fold, read before the minutes

grid_cells

How many cells the grid overlay_mesh lays over these parts at

Classes

Functions:

overlay_mesh

overlay_mesh(geometry: Any, ids, size: float, progress: Any = None, piece: bool = False, cuts_from=(), discard: Any = None) -> tuple[Any, dict]

The parts as eight-node bricks fitted to their shape, ready to join to a geometry (mesh.join).

Each part gets one element group, named for it (or for its kind and id when it has no name); a brick goes to the part whose recipe holds its center, the later part where parts overlap. Parts that touch share their nodes there.

Refused: a solid that is part of another, a size that is not positive, a mesh of more than MESH_LIMIT bricks by its volume, and a grid of more than GRID_LIMIT cells.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id; each a solid nothing else uses.

required
size float

The element size, as the geometry holds its coordinates: the grid's spacing, the block faces falling on grid planes.

required
progress callable

Told (done, total) as the mesh goes.

None
piece bool

The part is a union's operand meshed on its own (solids.brick_mesh).

False
cuts_from sequence of int

Solids whose flat faces the grid is cut at too, the earlier pieces of a union.

()
discard callable

Given the bricks' centers, (n, 3), which bricks the caller does not want: a union's later piece inside the pieces before it. They are left out of the mesh handed back, and a fold among them is no refusal: the rod of Brandon's volume folded four bricks at its end inside the block it entered (2026-10-20).

None

Returns:

Type Description
(Geometry, dict)

The bricks, in the geometry's length unit; and what was made: 'bricks' and 'nodes', the counts; 'quality', the worst scaled Jacobian (1 for a cube, 0 for a flat brick); 'poor', how many bricks are under 0.3; 'uncaptured', how many points along the sharp edges found no node of their own; 'sweeps', the final smoothing's; 'off_surface', how far the farthest surface node was let into the material to unfold a brick at a feather edge, as a share of the size (0 when none was); 'shortest', the shortest edge; and 'discarded', the bricks discard named.

Source code in src/visualdynamics/core/hexmesh.py
def overlay_mesh(geometry: Any, ids, size: float, progress: Any = None,
                 piece: bool = False, cuts_from=(), discard: Any = None) -> tuple[Any, dict]:
    """The parts as eight-node bricks fitted to their shape, ready to
    join to a geometry (`mesh.join`).

    Each part gets one element group, named for it (or for its kind and
    id when it has no name); a brick goes to the part whose recipe holds
    its center, the later part where parts overlap. Parts that touch
    share their nodes there.

    Refused: a solid that is part of another, a size that is not
    positive, a mesh of more than `MESH_LIMIT` bricks by its volume, and
    a grid of more than `GRID_LIMIT` cells.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id; each a solid nothing else uses.
    size : float
        The element size, as the geometry holds its coordinates: the
        grid's spacing, the block faces falling on grid planes.
    progress : callable, optional
        Told ``(done, total)`` as the mesh goes.
    piece : bool, default False
        The part is a union's operand meshed on its own (`solids.brick_mesh`).
    cuts_from : sequence of int, optional
        Solids whose flat faces the grid is cut at too, the earlier
        pieces of a union.
    discard : callable, optional
        Given the bricks' centers, (n, 3), which bricks the caller does
        not want: a union's later piece inside the pieces before it.
        They are left out of the mesh handed back, and a fold among
        them is no refusal: the rod of Brandon's volume folded four
        bricks at its end inside the block it entered (2026-10-20).

    Returns
    -------
    (Geometry, dict)
        The bricks, in the geometry's length unit; and what was made:
        'bricks' and 'nodes', the counts; 'quality', the worst scaled
        Jacobian (1 for a cube, 0 for a flat brick); 'poor', how many
        bricks are under 0.3; 'uncaptured', how many points along the
        sharp edges found no node of their own; 'sweeps', the final
        smoothing's; 'off_surface', how far the farthest surface node
        was let into the material to unfold a brick at a feather edge,
        as a share of the size (0 when none was); 'shortest', the
        shortest edge; and 'discarded', the bricks `discard` named.
    """
    ids = [int(i) for i in ids]
    if not ids:
        raise ValueError('pick a solid to mesh')
    if not piece:
        refuse_parts_of_another(geometry, ids)
    size = float(size)
    if not size > 0.0:
        raise ValueError('an element size above zero')
    ticker = Ticker(progress)
    ticker.add(3 + STAGES + FINAL_SWEEPS + 1)

    def fd(points):
        return _distance(geometry, ids, points)

    def fdg(points):
        return _distance_gradient(geometry, ids, points)

    low, high = _extent(geometry, ids)
    tol = 1e-9 * float(np.linalg.norm(high - low))
    xyz, hexes, groups = _grid(geometry, ids, size, cuts_from)
    if not len(hexes):
        raise ValueError('the parts hold nothing to mesh')
    ticker.tick()
    xyz, hexes, groups, faces, face_hex, surface = _pillow(xyz, hexes, groups, size)
    ticker.tick()
    mesh = _Mesh(xyz, hexes, faces, face_hex, surface, fd, size, tol, fdg)
    uncaptured = mesh.capture(geometry, ids)
    mesh.smooth_skin()
    mesh.aim()
    ticker.tick()
    start = mesh.xyz.copy()
    for stage in range(1, STAGES + 1):
        mesh.xyz[surface] = start[surface] + stage / STAGES * (mesh.target[surface] - start[surface])
        mesh.smooth(STAGE_SWEEPS, slide=False)
        ticker.tick()
    sweeps = mesh.smooth(FINAL_SWEEPS, slide=True, ticker=ticker)
    ticker.tick(FINAL_SWEEPS - sweeps)
    mesh.finish()
    off = mesh.untangle()
    xyz = mesh.xyz
    quality = _scaled_jacobians(xyz, hexes).min(axis=1)
    wanted = np.ones(len(hexes), dtype=bool)
    if discard is not None:
        wanted = ~np.asarray(discard(xyz[hexes].mean(axis=1)), dtype=bool)
    folded = int(((quality <= 0.0) & wanted).sum())
    if folded:
        # a feature two bricks across or less leaves no room to fit
        # (the 1 in side hole at 0.5 in): refused, not handed over
        raise ValueError(f'{folded:,} bricks folded fitting the shape at that size: '
                         'use a smaller one, or tetrahedra')
    discarded = int((~wanted).sum())
    hexes, groups, quality = hexes[wanted], groups[wanted], quality[wanted]
    keep = np.unique(hexes)
    renumber = np.zeros(len(xyz), dtype=int)
    renumber[keep] = np.arange(1, len(keep) + 1)
    shortest = float(np.linalg.norm(xyz[hexes[:, [b for _a, b in _HEX_EDGES]]]
                                    - xyz[hexes[:, [a for a, _b in _HEX_EDGES]]], axis=2).min())
    names, present = [], []
    for slot, part in enumerate(ids, start=1):
        if (groups == slot).any():
            item = solid(geometry, part)
            names.append(item.name.strip() or f'{item.kind.title()} {item.id}')
            present.append(slot)
    part = Geometry(node_id=np.arange(1, len(keep) + 1), node_xyz=xyz[keep],
                    elem_conn=list(renumber[hexes]), elem_type=[115] * len(hexes),
                    elem_group=[int(g) for g in groups], group_id=present,
                    group_name=names, length_unit=geometry.length_unit)
    ticker.tick()
    return part, {'bricks': len(hexes), 'nodes': len(keep), 'quality': float(quality.min()),
                  'poor': int((quality < 0.3).sum()), 'uncaptured': uncaptured,
                  'sweeps': sweeps, 'off_surface': off, 'shortest': shortest,
                  'discarded': discarded}

suggested_size

suggested_size(geometry: Any, ids, elements: str = 'hexes') -> float

An element size at which a mesh of the parts is likely to hold: for bricks fitted to a shape, the smallest radius over ACROSS (five bricks across it, where most fits measured have held and four has folded) and the thinnest wall over THROUGH; for tetrahedra, three across the radius and three through the wall. With no round feature and no wall, solids.mesh_size.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required
elements ('hexes', 'tetras')

The element type.

'hexes'

Returns:

Type Description
float
Source code in src/visualdynamics/core/hexmesh.py
def suggested_size(geometry: Any, ids, elements: str = 'hexes') -> float:
    """An element size at which a mesh of the parts is likely to hold:
    for bricks fitted to a shape, the smallest radius over `ACROSS`
    (five bricks across it, where most fits measured have held and four
    has folded) and the thinnest wall over `THROUGH`; for tetrahedra,
    three across the radius and three through the wall. With no round
    feature and no wall, `solids.mesh_size`.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.
    elements : {'hexes', 'tetras'}, default 'hexes'
        The element type.

    Returns
    -------
    float
    """
    from .solids import mesh_size

    radius, wall = smallest_feature(geometry, ids)
    across, through = (ACROSS, THROUGH) if elements == 'hexes' else (3.0, 3.0)
    size = min(radius / across, wall / through)
    return float(size) if np.isfinite(size) else mesh_size(geometry, ids)

likely_to_fold

likely_to_fold(geometry: Any, ids, size: float) -> dict

Whether a fit at size is likely to fold, read before the minutes are spent: how many bricks the smallest round feature's radius is across, and how many pillow bricks are flat or inside out as soon as the grid is pillowed, before any fitting.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required
size float

The element size, as the geometry holds its coordinates.

required

Returns:

Type Description
dict

'across', bricks across the smallest radius (inf with no round feature); 'through', bricks through the thinnest wall (inf with no wall); 'flat', the pillow bricks flat or inside out before fitting, said for what it is (0, uncounted, when the grid is past GRID_LIMIT or its bricks past MESH_LIMIT); 'cells', the grid's cells; 'bricks', about how many the mesh holds (brick_count); 'likely', True under four and a half across or under two and a half through; 'suggested', the size suggested_size gives.

Source code in src/visualdynamics/core/hexmesh.py
def likely_to_fold(geometry: Any, ids, size: float) -> dict:
    """Whether a fit at `size` is likely to fold, read before the minutes
    are spent: how many bricks the smallest round feature's radius is
    across, and how many pillow bricks are flat or inside out as soon
    as the grid is pillowed, before any fitting.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.
    size : float
        The element size, as the geometry holds its coordinates.

    Returns
    -------
    dict
        'across', bricks across the smallest radius (inf with no round
        feature); 'through', bricks through the thinnest wall (inf with
        no wall); 'flat', the pillow bricks flat or inside out before
        fitting, said for what it is (0, uncounted, when the grid is
        past `GRID_LIMIT` or its bricks past `MESH_LIMIT`); 'cells', the
        grid's cells; 'bricks', about how many the mesh holds
        (`brick_count`); 'likely', True
        under four and a half across or under two and a half through;
        'suggested', the size `suggested_size` gives.
    """
    ids = [int(i) for i in ids]
    radius, wall = smallest_feature(geometry, ids)
    across = radius / float(size)
    # the wall was read and then ignored until 2026-10-19: a 2 x 2
    # pattern of I-beams with 0.2 in flanges under the peanut block
    # passed the check at 0.2 in, one brick through the flanges, and
    # the fit ran seven minutes to a worst brick of 0.08
    through = wall / float(size)
    # a grid past the limit is not laid to count its flat bricks: the
    # fit will refuse it in a moment with the count (2026-10-20: the
    # check raised out of the pane at 2.3 million cells)
    cells = grid_cells(geometry, ids, float(size))
    bricks = brick_count(geometry, ids, float(size))
    flat = 0
    if cells <= GRID_LIMIT and bricks <= MESH_LIMIT:
        xyz, hexes, groups = _grid(geometry, ids, float(size))
        if len(hexes):
            xyz, hexes, _groups, _faces, _face_hex, _surface = _pillow(xyz, hexes, groups, float(size))
            flat = int((_scaled_jacobians(xyz, hexes).min(axis=1) <= 0.0).sum())
    # bricks flat before fitting are said, not judged: a fit opens some
    return {'across': float(across), 'through': float(through), 'flat': flat,
            'cells': int(cells), 'bricks': int(bricks),
            'likely': bool(across < ACROSS - 0.5 or through < THROUGH - 0.5),
            'suggested': suggested_size(geometry, ids)}

grid_cells

grid_cells(geometry: Any, ids, size: float, piece: bool = False) -> int

How many cells the grid overlay_mesh lays over these parts at size holds, voids included: the count before anything is asked of the shape, so a pane can decide whether to preview.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required
size float

The element size, as the geometry holds its coordinates.

required

Returns:

Type Description
int
Source code in src/visualdynamics/core/hexmesh.py
def grid_cells(geometry: Any, ids, size: float, piece: bool = False) -> int:
    """How many cells the grid `overlay_mesh` lays over these parts at
    `size` holds, voids included: the count before anything is asked
    of the shape, so a pane can decide whether to preview.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.
    size : float
        The element size, as the geometry holds its coordinates.

    Returns
    -------
    int
    """
    return int(np.prod([len(line) - 1 for line in _lines(geometry, [int(i) for i in ids], float(size))]))