Skip to content

visualdynamics.core.solids

solids

Solids: the shapes a part is made of, before it is meshed.

A part is described most directly by its shapes. The frame of the Frame-Wing example is a block with four windows and 41 holes taken out of it, and a wing is a block less eighteen holes. So a geometry holds a table of solids beside its nodes, coordinate systems and elements. Each one is a primitive (a block or a cylinder, or a section or a hole made of them, placed by a center and three angles), a boolean of others (union, subtract or intersect), or a pattern of copies of others. Solids are not elements: nothing that solves, animates or exports a geometry reads them, and meshing them (Mesh Solids, extrusion_mesh) is what turns them into elements.

The table holds recipes, the numbers a person typed, so a saved project reopens editable and needs no geometry kernel to load. Evaluating a solid (its volume, its surface for drawing, the boolean itself) is done by OpenCASCADE (shape), through the OCP bindings the STEP importer also uses, a dependency of the package. Whether a point is inside a solid, which is what meshing asks, is worked out from the recipes without the kernel (contains).

Lengths are held as the geometry holds its coordinates (meters once its units are defined), and angles in degrees about the geometry's fixed X, then Y, then Z axes, the rule the add pane turns a solid by (rotate.frame_from_angles).

Classes:

Name Description
Solid

One row of a geometry's solids table.

NotAnExtrusion

The parts cannot be meshed into bricks: a solid is turned off

Functions:

Name Description
add_block

Add a block to geometry's solids, widths across along its own

add_cylinder

Add a cylinder to geometry's solids, radius about its own Z

add_cone

Add a cone to geometry's solids: a frustum height along its own

add_sphere

Add a sphere to geometry's solids, radius about center.

add_plate

Add a plate to geometry's solids: a flat rectangle widths

add_disk

Add a disk to geometry's solids: a flat circle of radius in

is_flat

Whether a solid is flat: a plate or a disk, or a boolean or a

add_section

Add a section to geometry's solids: an I-beam, a channel, a tee,

add_hole

Add a hole to geometry's solids, to subtract from a part.

edit

Change a solid after it is made, in place.

move

Move solids whole, each with every solid it is made of, turned by

combine

Add a boolean of solids already there: their union, the first with

reorder

Put solids in a new order: the parts, or one solid's own.

nothing_left

Why a boolean leaves nothing (a subtraction whose first solid lies

delete

Delete solids, each with everything it is made of: a plate with

copy

Copy solids, recipe and all, into a geometry: geometry itself,

separate

Take booleans and patterns apart: each one is removed, and its

pattern

A pattern of solids: counts copies along the geometry's X, Y and

used_by

{solid id: the id of the boolean or pattern it is part of}, for

top_level

The solids no boolean uses: the parts as they stand, which are the

solid

The solid with this id; KeyError if there is none.

require_kernel

The OCP bindings, or a ValueError whose message says why they

shape

The solid as an OpenCASCADE shape, in the units the geometry holds

brick_count

About how many bricks a mesh of these parts at size holds:

volume

The solid's volume, in the cube of the geometry's length unit (m³

area

A flat solid's area, in the square of the geometry's length

shape_triangles

The corners of the triangles OpenCASCADE tessellates a shape's

contains

Which of points lie inside the solid, worked out from its recipe

distance

The signed distance from each of points to the solid's surface,

distance_gradient

distance and its gradient together: the unit direction out of

extrusion_axis

The axis (0, 1 or 2) the parts are extrusions along.

mesh_size

The element size an extrusion is meshed at by default: half the

smallest_feature

(the smallest radius, the thinnest wall) of the parts, in the

refuse_parts_of_another

Refuse a solid that is part of a boolean or a pattern: the one

grid_cells

How many bricks the grid extrusion_mesh would lay for these

extrusion_mesh

The parts as eight-node bricks, ready to join to a geometry

brick_mesh

The parts as eight-node bricks: extrusion_mesh where they are

piece_cuts

The pieces a brick mesh is made in, each with the solids cut

meshed_as

The id of the solid a piece is meshed as, while the block runs:

pieces

The pieces a brick mesh is made in when the parts together are

piece_name

The name a piece's element group takes: the solid's name, or

piece_plan

How brick_mesh would mesh these parts in pieces: for each

turned_sweep

A block, cylinder or section turned off the geometry's axes,

meshed_elements

The ids of the elements Mesh Solids makes, bricks, tetrahedra

Classes

Solid dataclass

Solid(id: int, name: str, kind: str, center: tuple[float, float, float] = (0.0, 0.0, 0.0), angles: tuple[float, float, float] = (0.0, 0.0, 0.0), widths: tuple[float, float, float] = (0.0, 0.0, 0.0), radius: float = 0.0, length: float = 0.0, operands: tuple[int, ...] = (), corner: float = 0.0, counts: tuple[int, int, int] = (1, 1, 1), dimensions: tuple[float, ...] = ())

One row of a geometry's solids table.

A primitive is placed by its center and turned by angles. A block is widths across along its own axes, and a cylinder is radius about its own Z axis and length along it. A section or a hole is sized by its dimensions (SECTIONS, HOLE_DIMENSIONS), and a section is length along its own Z. A boolean names its operands by id, in order (a subtraction takes the rest from the first), and its own numbers are unused. A pattern names its solids as a boolean does and holds counts copies along its own axes, widths apart, turned by angles; the copies follow the originals, and the pattern is changed as one.

Parameters:

Name Type Description Default
id int

Unique within the geometry.

required
name str

What a person calls it; may be empty.

required
kind str

One of PRIMITIVES, one of OPERATIONS, or PATTERN.

required
center tuple of float

The primitive's middle.

(0.0, 0.0, 0.0)
angles tuple of float

Degrees about the fixed X, then Y, then Z axes.

(0.0, 0.0, 0.0)
widths tuple of float

A block's three widths along its own axes, a plate's two along its own X and Y (and zero along Z); a pattern's spacing along its own.

(0.0, 0.0, 0.0)
radius float

A cylinder's; a cone's radius at its base (its own -Z end) and its height; a sphere's radius; a disk's radius, in its own XY plane.

0.0
length float

A cylinder's; a cone's radius at its base (its own -Z end) and its height; a sphere's radius; a disk's radius, in its own XY plane.

0.0
operands tuple of int

A boolean's solids, by id.

()
corner float

A block's corner radius, to which its four edges along its own Z are rounded, so a window cut with rounded corners leaves the fillets a part has there.

0.0
counts tuple of int

A pattern's copies along its own axes, the original counted.

(1, 1, 1)
dimensions tuple of float

A section's or a hole's, in the order SECTIONS and HOLE_DIMENSIONS name them; a cone's radius at its top.

()

Methods:

Name Description
scaled

The same solid with every length multiplied by factor, which

as_record

The solid as plain numbers, for a file.

from_record

The solid a file's record describes (as_record).

Methods:
scaled
scaled(factor: float) -> Solid

The same solid with every length multiplied by factor, which is what a change of the geometry's length unit does to it.

Source code in src/visualdynamics/core/solids.py
def scaled(self, factor: float) -> Solid:
    """The same solid with every length multiplied by `factor`, which
    is what a change of the geometry's length unit does to it."""
    return replace(self, center=tuple(float(c) * factor for c in self.center),
                   widths=tuple(float(w) * factor for w in self.widths),
                   radius=self.radius * factor,
                   length=self.length * factor,
                   corner=self.corner * factor,
                   dimensions=tuple(float(d) * factor for d in self.dimensions))
as_record
as_record() -> dict

The solid as plain numbers, for a file.

Source code in src/visualdynamics/core/solids.py
def as_record(self) -> dict:
    """The solid as plain numbers, for a file."""
    return {'id': self.id, 'name': self.name, 'kind': self.kind,
            'center': list(self.center), 'angles': list(self.angles),
            'widths': list(self.widths), 'radius': self.radius,
            'length': self.length, 'operands': list(self.operands),
            'corner': self.corner, 'counts': list(self.counts),
            'dimensions': list(self.dimensions)}
from_record classmethod
from_record(record: dict) -> Solid

The solid a file's record describes (as_record).

Source code in src/visualdynamics/core/solids.py
@classmethod
def from_record(cls, record: dict) -> Solid:
    """The solid a file's record describes (`as_record`)."""
    return cls(int(record['id']), str(record['name']), str(record['kind']),
               tuple(float(v) for v in record['center']),
               tuple(float(v) for v in record['angles']),
               tuple(float(v) for v in record['widths']),
               float(record['radius']), float(record['length']),
               tuple(int(v) for v in record['operands']),
               float(record.get('corner', 0.0)),
               tuple(int(v) for v in record.get('counts', (1, 1, 1))),
               tuple(float(v) for v in record.get('dimensions', ())))

NotAnExtrusion

Bases: ValueError

The parts cannot be meshed into bricks: a solid is turned off the geometry's axes. Tetrahedra can mesh them (tetmesh).

Functions:

add_block

add_block(geometry: Any, center, widths, angles=(0.0, 0.0, 0.0), name: str = '', corner: float = 0.0) -> int

Add a block to geometry's solids, widths across along its own axes, with its middle at center and turned by angles.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers each, as the geometry holds its coordinates; every width positive.

required
widths array_like

Three numbers each, as the geometry holds its coordinates; every width positive.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''
corner float

The radius its four edges along its own Z are rounded to, from zero to half the narrower of its widths along X and Y.

0

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_block(geometry: Any, center, widths, angles=(0.0, 0.0, 0.0),
              name: str = '', corner: float = 0.0) -> int:
    """Add a block to `geometry`'s solids, `widths` across along its own
    axes, with its middle at `center` and turned by `angles`.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center, widths : array_like
        Three numbers each, as the geometry holds its coordinates; every
        width positive.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.
    corner : float, default 0
        The radius its four edges along its own Z are rounded to, from
        zero to half the narrower of its widths along X and Y.

    Returns
    -------
    int
        The new solid's id.
    """
    solid = _block(_next_id(geometry), name, center, widths, angles, corner)
    geometry.solids.append(solid)
    return solid.id

add_cylinder

add_cylinder(geometry: Any, center, radius: float, length: float, angles=(0.0, 0.0, 0.0), name: str = '') -> int

Add a cylinder to geometry's solids, radius about its own Z axis and length along it, with its middle at center and turned by angles. With no angles its axis is the geometry's Z.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
radius float

As the geometry holds its coordinates; both positive.

required
length float

As the geometry holds its coordinates; both positive.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_cylinder(geometry: Any, center, radius: float, length: float,
                 angles=(0.0, 0.0, 0.0), name: str = '') -> int:
    """Add a cylinder to `geometry`'s solids, `radius` about its own Z
    axis and `length` along it, with its middle at `center` and turned by
    `angles`. With no angles its axis is the geometry's Z.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    radius, length : float
        As the geometry holds its coordinates; both positive.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    solid = _cylinder(_next_id(geometry), name, center, radius, length, angles)
    geometry.solids.append(solid)
    return solid.id

add_cone

add_cone(geometry: Any, center, base_radius: float, top_radius: float, height: float, angles=(0.0, 0.0, 0.0), name: str = '') -> int

Add a cone to geometry's solids: a frustum height along its own Z, base_radius at its -Z end and top_radius at its +Z end, either of them zero for a point, with its middle at center and turned by angles. With no angles its axis is the geometry's Z.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
base_radius float

Not negative, not both zero, and not equal (that is a cylinder).

required
top_radius float

Not negative, not both zero, and not equal (that is a cylinder).

required
height float

Positive.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_cone(geometry: Any, center, base_radius: float, top_radius: float, height: float,
             angles=(0.0, 0.0, 0.0), name: str = '') -> int:
    """Add a cone to `geometry`'s solids: a frustum `height` along its own
    Z, `base_radius` at its -Z end and `top_radius` at its +Z end, either
    of them zero for a point, with its middle at `center` and turned by
    `angles`. With no angles its axis is the geometry's Z.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    base_radius, top_radius : float
        Not negative, not both zero, and not equal (that is a cylinder).
    height : float
        Positive.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    # Brandon, 2026-10-20: blocks, sections, cylinders, cones and spheres
    # to build parts from
    solid = _cone(_next_id(geometry), name, center, base_radius, top_radius, height, angles)
    geometry.solids.append(solid)
    return solid.id

add_sphere

add_sphere(geometry: Any, center, radius: float, name: str = '') -> int

Add a sphere to geometry's solids, radius about center.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
radius float

Positive.

required
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_sphere(geometry: Any, center, radius: float, name: str = '') -> int:
    """Add a sphere to `geometry`'s solids, `radius` about `center`.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    radius : float
        Positive.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    solid = _sphere(_next_id(geometry), name, center, radius)
    geometry.solids.append(solid)
    return solid.id

add_plate

add_plate(geometry: Any, center, widths, angles=(0.0, 0.0, 0.0), name: str = '') -> int

Add a plate to geometry's solids: a flat rectangle widths across along its own X and Y, in its own XY plane, with its middle at center and turned by angles. With no angles it lies in the geometry's XY plane.

A plate has no thickness of its own. It is meshed into plates (Mesh Solids, Shells), and the thickness is set on their element group. Solids subtracted from it cut it where they cross its plane.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
widths array_like

Two numbers, both positive: along its own X, then Y.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_plate(geometry: Any, center, widths, angles=(0.0, 0.0, 0.0),
              name: str = '') -> int:
    """Add a plate to `geometry`'s solids: a flat rectangle `widths`
    across along its own X and Y, in its own XY plane, with its middle at
    `center` and turned by `angles`. With no angles it lies in the
    geometry's XY plane.

    A plate has no thickness of its own. It is meshed into plates
    (Mesh Solids, Shells), and the thickness is set on their element
    group. Solids subtracted from it cut it where they cross its plane.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    widths : array_like
        Two numbers, both positive: along its own X, then Y.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    solid = _plate(_next_id(geometry), name, center, widths, angles)
    geometry.solids.append(solid)
    return solid.id

add_disk

add_disk(geometry: Any, center, radius: float, angles=(0.0, 0.0, 0.0), name: str = '') -> int

Add a disk to geometry's solids: a flat circle of radius in its own XY plane, centered at center and turned by angles. With no angles it lies in the geometry's XY plane.

A disk has no thickness of its own; as a plate's, it is set on the element group it is meshed into (Mesh Solids, Shells).

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
radius float

Positive.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_disk(geometry: Any, center, radius: float, angles=(0.0, 0.0, 0.0),
             name: str = '') -> int:
    """Add a disk to `geometry`'s solids: a flat circle of `radius` in
    its own XY plane, centered at `center` and turned by `angles`. With
    no angles it lies in the geometry's XY plane.

    A disk has no thickness of its own; as a plate's, it is set on the
    element group it is meshed into (Mesh Solids, Shells).

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    radius : float
        Positive.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    solid = _disk(_next_id(geometry), name, center, radius, angles)
    geometry.solids.append(solid)
    return solid.id

is_flat

is_flat(geometry: Any, solid_id: int) -> bool

Whether a solid is flat: a plate or a disk, or a boolean or a pattern whose material is one, such as a plate with holes subtracted from it. A flat solid is meshed into plates on its plane, and a union of flat and solid ones is refused when it is made.

Parameters:

Name Type Description Default
geometry Geometry

Whose solid.

required
solid_id int

Which.

required

Returns:

Type Description
bool
Source code in src/visualdynamics/core/solids.py
def is_flat(geometry: Any, solid_id: int) -> bool:
    """Whether a solid is flat: a plate or a disk, or a boolean or a
    pattern whose material is one, such as a plate with holes subtracted
    from it. A flat solid is meshed into plates on its plane, and a
    union of flat and solid ones is refused when it is made.

    Parameters
    ----------
    geometry : Geometry
        Whose solid.
    solid_id : int
        Which.

    Returns
    -------
    bool
    """
    item = solid(geometry, solid_id)
    if item.kind in FLAT:
        return True
    if item.kind in ('subtract', 'intersect'):
        return is_flat(geometry, item.operands[0])
    if item.kind in ('union', PATTERN):
        return all(is_flat(geometry, o) for o in item.operands)
    return False

add_section

add_section(geometry: Any, shape: str, center, dimensions, length: float, angles=(0.0, 0.0, 0.0), name: str = '') -> int

Add a section to geometry's solids: an I-beam, a channel, a tee, an angle, or a rectangular or round tube, length along its own Z.

A section is one row, sized by its own dimensions, where a union of blocks would be three rows changed one at a time. Its middle (the middle of the box it fills) is at center, and with no angles it runs along the geometry's Z.

Across, along its own axes: an I-beam's or a tee's depth runs along Y and its flange width along X, with a tee's flange at +Y; a channel's flanges run along +X from its web; an angle's long leg runs along +X and its short leg along +Y from the heel; a rectangular tube is width along X and height along Y. The dimensions are checked by the beam section's rules (fem.Section.of_shape), so a shape refused as a beam's section is refused here, and a round tube's wall must also be under half its outer diameter.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
shape str

One of SECTIONS.

required
center array_like

Three numbers, as the geometry holds its coordinates.

required
dimensions sequence of float

In the order SECTIONS[shape] names them, as the geometry holds its coordinates.

required
length float

As the geometry holds its coordinates; positive.

required
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_section(geometry: Any, shape: str, center, dimensions, length: float,
                angles=(0.0, 0.0, 0.0), name: str = '') -> int:
    """Add a section to `geometry`'s solids: an I-beam, a channel, a tee,
    an angle, or a rectangular or round tube, `length` along its own Z.

    A section is one row, sized by its own dimensions, where a union of
    blocks would be three rows changed one at a time. Its middle (the
    middle of the box it fills) is at `center`, and with no `angles` it
    runs along the geometry's Z.

    Across, along its own axes: an I-beam's or a tee's `depth` runs along
    Y and its `flange width` along X, with a tee's flange at +Y; a
    channel's flanges run along +X from its web; an angle's long leg runs
    along +X and its short leg along +Y from the heel; a rectangular tube
    is `width` along X and `height` along Y. The dimensions are checked by
    the beam section's rules (`fem.Section.of_shape`), so a shape refused
    as a beam's section is refused here, and a round tube's wall must
    also be under half its outer diameter.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    shape : str
        One of `SECTIONS`.
    center : array_like
        Three numbers, as the geometry holds its coordinates.
    dimensions : sequence of float
        In the order `SECTIONS[shape]` names them, as the geometry holds
        its coordinates.
    length : float
        As the geometry holds its coordinates; positive.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.

    Returns
    -------
    int
        The new solid's id.
    """
    # a section as one row, sized by its own dimensions (2026-10-12)
    if shape not in SECTIONS:
        raise ValueError(f'{shape!r} is not one of {tuple(SECTIONS)}')
    solid = _shaped(_next_id(geometry), name, shape, center, dimensions, length,
                    angles)
    geometry.solids.append(solid)
    return solid.id

add_hole

add_hole(geometry: Any, center, diameter: float, depth: float | None = None, angles=(0.0, 0.0, 0.0), name: str = '', counterbore=None) -> int

Add a hole to geometry's solids, to subtract from a part.

The hole enters at center, on the face it is drilled from, and runs depth along its own -Z (down the geometry's Z with no angles). With no depth it runs through the part it is cut from, however thick that part is later made. A counterbore is a wider, shallower bore at the entry.

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
center array_like

Where it enters, three numbers as the geometry holds its coordinates.

required
diameter float

As the geometry holds its coordinates; positive.

required
depth float

As the geometry holds its coordinates; None or zero for a hole through the part.

None
angles array_like

Degrees about the fixed X, then Y, then Z axes.

(0, 0, 0)
name str

What to call it.

''
counterbore (float, float)

Its diameter, wider than the hole's, and its depth, positive and (for a blind hole) shallower than the hole's.

None

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def add_hole(geometry: Any, center, diameter: float, depth: float | None = None,
             angles=(0.0, 0.0, 0.0), name: str = '',
             counterbore=None) -> int:
    """Add a hole to `geometry`'s solids, to subtract from a part.

    The hole enters at `center`, on the face it is drilled from, and runs
    `depth` along its own -Z (down the geometry's Z with no `angles`).
    With no depth it runs through the part it is cut from, however thick
    that part is later made. A counterbore is a wider, shallower bore at
    the entry.

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    center : array_like
        Where it enters, three numbers as the geometry holds its
        coordinates.
    diameter : float
        As the geometry holds its coordinates; positive.
    depth : float, optional
        As the geometry holds its coordinates; None or zero for a hole
        through the part.
    angles : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes.
    name : str, optional
        What to call it.
    counterbore : (float, float), optional
        Its diameter, wider than the hole's, and its depth, positive and
        (for a blind hole) shallower than the hole's.

    Returns
    -------
    int
        The new solid's id.
    """
    # a hole as a solid to subtract (2026-10-12)
    wide, deep = (0.0, 0.0) if counterbore is None else counterbore
    solid = _shaped(_next_id(geometry), name, HOLE, center,
                    (diameter, 0.0 if depth is None else depth, wide, deep), 0.0,
                    angles)
    geometry.solids.append(solid)
    return solid.id

edit

edit(geometry: Any, solid_id: int, **changes) -> None

Change a solid after it is made, in place.

What can be changed is a block's center, angles, widths and corner; a cylinder's center, angles, radius and length; a cone's center, angles, radius (at its base), top radius and length (its height); a sphere's center and radius; a plate's center, angles and widths; a disk's center, angles and radius; a section's center, angles, dimensions and length; a hole's center, angles and dimensions; a pattern's counts, spacing and angles; and any solid's name. The rest are kept. The solid keeps its id, so a boolean it is part of takes the change: a hole made larger is a larger hole in the plate it was subtracted from. A mesh already made from it is not changed; running Mesh Solids again meshes the change.

Parameters:

Name Type Description Default
geometry Geometry

Whose solid; changed in place.

required
solid_id int

Which.

required
**changes

The numbers to change, as add_block, add_cylinder, add_section and pattern take them (a hole's dimensions in the order of HOLE_DIMENSIONS, a depth of zero for through), and name.

{}

Raises:

Type Description
ValueError

For a number its kind does not have (a boolean's shape is its solids', changed one at a time, and a boolean is moved whole by move), or a value an add would refuse.

KeyError

If there is no solid solid_id.

Source code in src/visualdynamics/core/solids.py
def edit(geometry: Any, solid_id: int, **changes) -> None:
    """Change a solid after it is made, in place.

    What can be changed is a block's center, angles, widths and corner; a
    cylinder's center, angles, radius and length; a cone's center,
    angles, radius (at its base), top radius and length (its height); a
    sphere's center and radius; a plate's center, angles and widths; a
    disk's center, angles and radius; a section's center,
    angles, dimensions and length; a hole's center, angles and
    dimensions; a pattern's counts, spacing and angles; and any solid's
    name. The rest are kept. The solid keeps its id, so a boolean it is
    part of takes the change: a hole made larger is a larger hole in the
    plate it was subtracted from. A mesh already made from it is not
    changed; running Mesh Solids again meshes the change.

    Parameters
    ----------
    geometry : Geometry
        Whose solid; changed in place.
    solid_id : int
        Which.
    **changes
        The numbers to change, as `add_block`, `add_cylinder`,
        `add_section` and `pattern` take them (a hole's `dimensions` in
        the order of `HOLE_DIMENSIONS`, a depth of zero for through), and
        `name`.

    Raises
    ------
    ValueError
        For a number its kind does not have (a boolean's shape is its
        solids', changed one at a time, and a boolean is moved whole by
        `move`), or a value an add would refuse.
    KeyError
        If there is no solid `solid_id`.
    """
    # move it, size it, reshape it (Brandon, 2026-10-11)
    item = solid(geometry, solid_id)
    allowed = EDITABLE.get(item.kind, ()) + ('name',)
    wrong = [key for key in changes if key not in allowed]
    if wrong:
        if item.kind in OPERATIONS:
            raise ValueError(f'solid {solid_id} is a {item.kind}: change the '
                             'solids it is made of, or move it whole')
        from .fem import with_article

        raise ValueError(f'{with_article(item.kind)} has no {wrong[0]}')
    now = {'name': item.name, 'center': item.center, 'angles': item.angles,
           'widths': item.widths, 'corner': item.corner, 'radius': item.radius,
           'length': item.length, 'counts': item.counts, 'spacing': item.widths,
           'dimensions': item.dimensions,
           'top_radius': _top(item) if item.kind == 'cone' else 0.0, **changes}
    if item.kind == 'block':
        changed = _block(item.id, now['name'], now['center'], now['widths'],
                         now['angles'], now['corner'])
    elif item.kind == 'cylinder':
        changed = _cylinder(item.id, now['name'], now['center'], now['radius'],
                            now['length'], now['angles'])
    elif item.kind == 'cone':
        changed = _cone(item.id, now['name'], now['center'], now['radius'],
                        now['top_radius'], now['length'], now['angles'])
    elif item.kind == 'sphere':
        changed = _sphere(item.id, now['name'], now['center'], now['radius'])
    elif item.kind == 'plate':
        changed = _plate(item.id, now['name'], now['center'], now['widths'],
                         now['angles'])
    elif item.kind == 'disk':
        changed = _disk(item.id, now['name'], now['center'], now['radius'],
                        now['angles'])
    elif item.kind in SHAPED:
        changed = _shaped(item.id, now['name'], item.kind, now['center'],
                          now['dimensions'], now['length'], now['angles'])
    elif item.kind == PATTERN:
        changed = _pattern(item.id, now['name'], item.operands, now['counts'],
                           now['spacing'], now['angles'])
    else:
        changed = replace(item, name=now['name'])
    geometry.solids[geometry.solids.index(item)] = changed

move

move(geometry: Any, ids, offset=(0.0, 0.0, 0.0), turn=(0.0, 0.0, 0.0)) -> int

Move solids whole, each with every solid it is made of, turned by turn about the middle of their extent and then moved by offset.

A boolean has no position of its own to change, so its solids are moved, each center turned about that middle and each solid turned by the same angles, and the part is the same shape in a new place. A pattern's originals move, and its axes turn with them.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids; changed in place.

required
ids sequence of int

The solids, by id.

required
offset array_like

How far, as the geometry holds its coordinates.

(0, 0, 0)
turn array_like

Degrees about the fixed X, then Y, then Z axes, as a solid's angles are.

(0, 0, 0)

Returns:

Type Description
int

How many primitives moved.

Source code in src/visualdynamics/core/solids.py
def move(geometry: Any, ids, offset=(0.0, 0.0, 0.0),
         turn=(0.0, 0.0, 0.0)) -> int:
    """Move solids whole, each with every solid it is made of, turned by
    `turn` about the middle of their extent and then moved by `offset`.

    A boolean has no position of its own to change, so its solids are
    moved, each center turned about that middle and each solid turned by
    the same angles, and the part is the same shape in a new place. A
    pattern's originals move, and its axes turn with them.

    Parameters
    ----------
    geometry : Geometry
        Whose solids; changed in place.
    ids : sequence of int
        The solids, by id.
    offset : array_like, default (0, 0, 0)
        How far, as the geometry holds its coordinates.
    turn : array_like, default (0, 0, 0)
        Degrees about the fixed X, then Y, then Z axes, as a solid's
        angles are.

    Returns
    -------
    int
        How many primitives moved.
    """
    from ..rotate import angles_of, frame_from_angles

    ids = [int(i) for i in ids]
    if not ids:
        raise ValueError('pick a solid to move')
    offset = np.asarray(_numbers(offset, 3, 'an offset'))
    turn = _numbers(turn, 3, 'a turn')
    low, high = zip(*(_box(geometry, i) for i in ids))
    middle = (np.min(low, axis=0) + np.max(high, axis=0)) / 2.0
    # a turned vector, as a row: v @ R^T, and frame_from_angles keeps R^T
    rows = frame_from_angles(turn)[:3]

    def turned(item):
        frame = np.zeros((4, 3))
        frame[:3] = _frame(item) @ rows
        return tuple(float(a) for a in angles_of(frame))

    # the solids as stored, each once: a pattern's copies are where its
    # originals are, along its own axes, so the originals move and the
    # pattern's axes turn with them
    stored, stack = {}, list(ids)
    while stack:
        item = solid(geometry, stack.pop())
        if item.id not in stored:
            stored[item.id] = item
            stack.extend(item.operands)
    moved = 0
    for item in stored.values():
        if item.kind in PRIMITIVES:
            center = np.asarray(item.center)
            angles = item.angles
            if any(turn):
                center = middle + (center - middle) @ rows
                angles = turned(item)
            changed = replace(item, center=tuple(float(c) for c in center + offset),
                              angles=angles)
            moved += 1
        elif item.kind == PATTERN and any(turn):
            changed = replace(item, angles=turned(item))
        else:
            continue
        geometry.solids[geometry.solids.index(item)] = changed
    return moved

combine

combine(geometry: Any, operation: str, ids, name: str = '') -> int

Add a boolean of solids already there: their union, the first with the rest subtracted from it, or what they share.

Each solid goes into one boolean only, so a solid already part of another is refused. So is a boolean that would leave nothing: a subtraction whose first solid lies inside the rest, or an intersection of solids that do not meet. The operands become part of the new solid, and the table lists only the solids nothing else uses (top_level).

Parameters:

Name Type Description Default
geometry Geometry

Where it goes; changed in place.

required
operation str

One of OPERATIONS.

required
ids sequence of int

Two solids or more, by id, the first first.

required
name str

What to call it; the first solid's name when not given, so a plate named 'frame' with its holes subtracted is still 'frame', and so is the element group it meshes into.

''

Returns:

Type Description
int

The new solid's id.

Source code in src/visualdynamics/core/solids.py
def combine(geometry: Any, operation: str, ids, name: str = '') -> int:
    """Add a boolean of solids already there: their union, the first with
    the rest subtracted from it, or what they share.

    Each solid goes into one boolean only, so a solid already part of
    another is refused. So is a boolean that would leave nothing: a
    subtraction whose first solid lies inside the rest, or an
    intersection of solids that do not meet. The operands become part of the new solid, and
    the table lists only the solids nothing else uses (`top_level`).

    Parameters
    ----------
    geometry : Geometry
        Where it goes; changed in place.
    operation : str
        One of `OPERATIONS`.
    ids : sequence of int
        Two solids or more, by id, the first first.
    name : str, optional
        What to call it; the first solid's name when not given, so a
        plate named 'frame' with its holes subtracted is still 'frame',
        and so is the element group it meshes into.

    Returns
    -------
    int
        The new solid's id.
    """
    if operation not in OPERATIONS:
        raise ValueError(f'{operation!r} is not one of {OPERATIONS}')
    ids = tuple(int(i) for i in ids)
    if len(ids) < 2 or len(set(ids)) < len(ids):
        raise ValueError('a boolean takes two solids or more, each once')
    there = {s.id for s in geometry.solids}
    missing = [i for i in ids if i not in there]
    if missing:
        raise ValueError(f'no solid {missing[0]}')
    used = used_by(geometry)
    taken = [i for i in ids if i in used]
    if taken:
        raise ValueError(f'solid {taken[0]} is already part of solid '
                         f'{used[taken[0]]}')
    flat = [is_flat(geometry, i) for i in ids]
    if operation == 'union' and any(flat) and not all(flat):
        raise ValueError('a union is of plates and disks, or of solids with a '
                         'volume, not of both: they mesh into different elements')
    if operation != 'union' and any(flat[1:]):
        raise ValueError(f'solid {ids[flat[1:].index(True) + 1]} is flat and has no '
                         f'volume to {"take away" if operation == "subtract" else "share"}')
    name = name or solid(geometry, ids[0]).name
    made = Solid(_next_id(geometry), name, operation, operands=ids)
    before = list(geometry.solids)
    geometry.solids.append(made)
    _refuse_nothing_left(geometry, made.id, before,
                         f'the {operation} cannot be made')
    return made.id

reorder

reorder(geometry: Any, ids, within: int | None = None) -> None

Put solids in a new order: the parts, or one solid's own.

The order is what a boolean made from a pick reads first to last, and a subtraction takes every solid after the first from the first, so which solid comes first decides what is cut. With within, the ids are that boolean's (or pattern's) solids in their new order, and an existing subtraction then cuts the new first solid. Without it, they are the parts (top_level) in their new order; the solids a boolean uses stay where they are.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids; changed in place.

required
ids sequence of int

Every one of the solids being ordered, each once, in the order wanted.

required
within int

The boolean or pattern whose solids these are.

None

Raises:

Type Description
ValueError

When ids is not exactly those solids, within has none, or the new order would leave the boolean empty.

Source code in src/visualdynamics/core/solids.py
def reorder(geometry: Any, ids, within: int | None = None) -> None:
    """Put solids in a new order: the parts, or one solid's own.

    The order is what a boolean made from a pick reads first to last, and
    a subtraction takes every solid after the first from the first, so
    which solid comes first decides what is cut. With `within`, the ids
    are that boolean's (or pattern's) solids in their new order, and an
    existing subtraction then cuts the new first solid. Without it, they
    are the parts (`top_level`) in their new order; the solids a boolean
    uses stay where they are.

    Parameters
    ----------
    geometry : Geometry
        Whose solids; changed in place.
    ids : sequence of int
        Every one of the solids being ordered, each once, in the order
        wanted.
    within : int, optional
        The boolean or pattern whose solids these are.

    Raises
    ------
    ValueError
        When `ids` is not exactly those solids, `within` has none, or
        the new order would leave the boolean empty.
    """
    ids = [int(i) for i in ids]
    if within is None:
        parts = [item.id for item in top_level(geometry)]
        if sorted(ids) != sorted(parts) or len(set(ids)) != len(ids):
            raise ValueError(f'the parts are {parts}; give each of them '
                             'once, in the order wanted')
        order = iter(solid(geometry, i) for i in ids)
        used = used_by(geometry)
        geometry.solids = [item if item.id in used else next(order)
                           for item in geometry.solids]
        return
    holder = solid(geometry, within)
    if not holder.operands:
        raise ValueError(f'solid {within} is not made of other solids')
    if sorted(ids) != sorted(holder.operands) or len(set(ids)) != len(ids):
        raise ValueError(f'solid {within} is made of {list(holder.operands)}; '
                         'give each of them once, in the order wanted')
    before = geometry.solids
    geometry.solids = [replace(item, operands=tuple(ids))
                       if item.id == holder.id else item
                       for item in geometry.solids]
    _refuse_nothing_left(geometry, holder.id, before,
                         f'solid {ids[0]} cannot come first')

nothing_left

nothing_left(geometry: Any, solid_id: int) -> str | None

Why a boolean leaves nothing (a subtraction whose first solid lies inside the rest, an intersection of solids that do not meet), or None when something is left.

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required

Returns:

Type Description
str or None
Source code in src/visualdynamics/core/solids.py
def nothing_left(geometry: Any, solid_id: int) -> str | None:
    """Why a boolean leaves nothing (a subtraction whose first solid lies
    inside the rest, an intersection of solids that do not meet), or
    None when something is left.

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.

    Returns
    -------
    str or None
    """
    from OCP.TopAbs import TopAbs_FACE
    from OCP.TopExp import TopExp_Explorer

    if TopExp_Explorer(shape(geometry, solid_id), TopAbs_FACE).More():
        return None
    item = solid(geometry, solid_id)
    first, rest = item.operands[0], item.operands[1:]
    others = ', '.join(str(i) for i in rest)
    if item.kind == 'subtract':
        return (f'taking solid{"s" * (len(rest) > 1)} {others} from solid '
                f'{first} leaves nothing')
    if item.kind == 'intersect':
        return f'solids {first}, {others} share nothing'
    return f'solid {solid_id} leaves nothing'

delete

delete(geometry: Any, ids) -> int

Delete solids, each with everything it is made of: a plate with holes cut in it is deleted whole, with the cylinders that cut them. A solid that a boolean still uses is refused, by name, unless that boolean is deleted with it; take the boolean apart first (separate).

Parameters:

Name Type Description Default
geometry Geometry

Whose solids; changed in place.

required
ids sequence of int

The solids, by id.

required

Returns:

Type Description
int

How many solids went, those inside included.

Source code in src/visualdynamics/core/solids.py
def delete(geometry: Any, ids) -> int:
    """Delete solids, each with everything it is made of: a plate with
    holes cut in it is deleted whole, with the cylinders that cut them.
    A solid that a boolean still uses is refused, by name, unless that
    boolean is deleted with it; take the boolean apart first
    (`separate`).

    Parameters
    ----------
    geometry : Geometry
        Whose solids; changed in place.
    ids : sequence of int
        The solids, by id.

    Returns
    -------
    int
        How many solids went, those inside included.
    """
    going = set()
    stack = list(_checked(geometry, ids))
    while stack:
        current = stack.pop()
        if current not in going:
            going.add(current)
            stack.extend(solid(geometry, current).operands)
    geometry.solids = [s for s in geometry.solids if s.id not in going]
    return len(going)

copy

copy(geometry: Any, ids, into: Any = None) -> list[int]

Copy solids, recipe and all, into a geometry: geometry itself, or into. Returns the new top-level ids, in the order of ids.

A boolean or a pattern is copied with every solid it is made of, each under a new id, so the copy is its own: editing a cylinder of the copy leaves the original. A named solid's copy is named ' copy'; the solids inside it keep their names. The two geometries have to hold their coordinates the same way, both with their units defined or neither.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The solids to copy, by id.

required
into Geometry

Where the copies go; geometry when omitted.

None

Returns:

Type Description
list of int

Raises:

Type Description
ValueError

With no ids, an id the geometry does not hold, or geometries whose units differ.

Source code in src/visualdynamics/core/solids.py
def copy(geometry: Any, ids, into: Any = None) -> list[int]:
    """Copy solids, recipe and all, into a geometry: `geometry` itself,
    or `into`. Returns the new top-level ids, in the order of `ids`.

    A boolean or a pattern is copied with every solid it is made of,
    each under a new id, so the copy is its own: editing a cylinder of
    the copy leaves the original. A named solid's copy is named
    '<name> copy'; the solids inside it keep their names. The two
    geometries have to hold their coordinates the same way, both with
    their units defined or neither.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The solids to copy, by id.
    into : Geometry, optional
        Where the copies go; `geometry` when omitted.

    Returns
    -------
    list of int

    Raises
    ------
    ValueError
        With no ids, an id the geometry does not hold, or geometries
        whose units differ.
    """
    # Copy and Paste on solid rows (Brandon, 2026-10-18)
    ids = [int(i) for i in ids]
    if not ids:
        raise ValueError('pick a solid to copy')
    target = geometry if into is None else into
    if (geometry.length_unit is None) != (target.length_unit is None):
        raise ValueError('the geometries hold their coordinates in different '
                         'units: define both or neither before copying '
                         'solids between them')
    for i in ids:
        solid(geometry, i)      # refused by name when it is not there
    made = []
    for i in ids:
        original = solid(geometry, i)
        new = _clone(geometry, original, target)
        if original.name.strip():
            target.solids[-1] = replace(new, name=f'{original.name.strip()} copy')
        made.append(new.id)
    return made

separate

separate(geometry: Any, ids) -> int

Take booleans and patterns apart: each one is removed, and its operands stand as parts again, which undoes a subtraction made by mistake. A primitive has nothing to take apart and is refused, and so is a solid that is still part of another.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids; changed in place.

required
ids sequence of int

The booleans or patterns, by id.

required

Returns:

Type Description
int

How many solids stand again.

Source code in src/visualdynamics/core/solids.py
def separate(geometry: Any, ids) -> int:
    """Take booleans and patterns apart: each one is removed, and its
    operands stand as parts again, which undoes a subtraction made by
    mistake. A primitive has nothing to take apart and is refused, and so
    is a solid that is still part of another.

    Parameters
    ----------
    geometry : Geometry
        Whose solids; changed in place.
    ids : sequence of int
        The booleans or patterns, by id.

    Returns
    -------
    int
        How many solids stand again.
    """
    ids = _checked(geometry, ids)
    plain = sorted(i for i in ids if solid(geometry, i).kind in PRIMITIVES)
    if plain:
        raise ValueError(f'solid {plain[0]} is a {solid(geometry, plain[0]).kind}, '
                         'with nothing to take apart')
    freed = sum(len(solid(geometry, i).operands) for i in ids)
    geometry.solids = [s for s in geometry.solids if s.id not in ids]
    return freed

pattern

pattern(geometry: Any, ids, counts, spacing) -> int

A pattern of solids: counts copies along the geometry's X, Y and Z, spacing apart, with the originals as the first of each.

Thirteen holes along a rail are one hole and counts of (13, 1, 1) at the insert pitch. The pattern is one solid, made of the originals as a boolean is, so changing the hole changes every copy, and the pattern's counts and spacing are changed as one (edit). It takes its first solid's name, as a boolean does. A pattern of a pattern is a grid of grids: the row of 13, then two rows across the rails. More than PATTERN_LIMIT (10,000) copies is refused.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids; changed in place.

required
ids sequence of int

The solids to copy, by id: solids nothing else uses.

required
counts sequence of int

How many along X, Y and Z, the original included; each one or more.

required
spacing sequence of float

How far apart along X, Y and Z, as the geometry holds its coordinates; it may be zero only along an axis with one copy.

required

Returns:

Type Description
int

The pattern's id.

Source code in src/visualdynamics/core/solids.py
def pattern(geometry: Any, ids, counts, spacing) -> int:
    """A pattern of solids: `counts` copies along the geometry's X, Y and
    Z, `spacing` apart, with the originals as the first of each.

    Thirteen holes along a rail are one hole and counts of (13, 1, 1) at
    the insert pitch. The pattern is one solid, made of the originals as
    a boolean is, so changing the hole changes every copy, and the
    pattern's counts and spacing are changed as one (`edit`). It takes
    its first solid's name, as a boolean does. A pattern of a pattern is
    a grid of grids: the row of 13, then two rows across the rails. More
    than `PATTERN_LIMIT` (10,000) copies is refused.

    Parameters
    ----------
    geometry : Geometry
        Whose solids; changed in place.
    ids : sequence of int
        The solids to copy, by id: solids nothing else uses.
    counts : sequence of int
        How many along X, Y and Z, the original included; each one or
        more.
    spacing : sequence of float
        How far apart along X, Y and Z, as the geometry holds its
        coordinates; it may be zero only along an axis with one copy.

    Returns
    -------
    int
        The pattern's id.
    """
    # one solid, made of the originals as a boolean is (Brandon,
    # 2026-10-11)
    ids = tuple(int(i) for i in ids)
    if not ids or len(set(ids)) < len(ids):
        raise ValueError('a pattern takes one solid or more, each once')
    used = used_by(geometry)
    for i in ids:
        solid(geometry, i)
        if i in used:
            raise ValueError(f'solid {i} is part of solid {used[i]}; pattern '
                             'that one')
    made = _pattern(_next_id(geometry), solid(geometry, ids[0]).name, ids, counts,
                    spacing, (0.0, 0.0, 0.0))
    geometry.solids.append(made)
    return made.id

used_by

used_by(geometry: Any) -> dict[int, int]

{solid id: the id of the boolean or pattern it is part of}, for every solid that is an operand of another.

Source code in src/visualdynamics/core/solids.py
def used_by(geometry: Any) -> dict[int, int]:
    """{solid id: the id of the boolean or pattern it is part of}, for
    every solid that is an operand of another."""
    return {operand: s.id for s in geometry.solids for operand in s.operands}

top_level

top_level(geometry: Any) -> list[Solid]

The solids no boolean uses: the parts as they stand, which are the ones a person sees, edits and meshes.

Source code in src/visualdynamics/core/solids.py
def top_level(geometry: Any) -> list[Solid]:
    """The solids no boolean uses: the parts as they stand, which are the
    ones a person sees, edits and meshes."""
    used = used_by(geometry)
    return [s for s in geometry.solids if s.id not in used]

solid

solid(geometry: Any, solid_id: int) -> Solid

The solid with this id; KeyError if there is none.

Source code in src/visualdynamics/core/solids.py
def solid(geometry: Any, solid_id: int) -> Solid:
    """The solid with this id; KeyError if there is none."""
    for s in geometry.solids:
        if s.id == int(solid_id):
            return s
    raise KeyError(f'no solid {solid_id}')

require_kernel

require_kernel()

The OCP bindings, or a ValueError whose message says why they did not import and how to reinstall them.

Source code in src/visualdynamics/core/solids.py
def require_kernel():
    """The OCP bindings, or a ValueError whose message says why they
    did not import and how to reinstall them."""
    # A dependency since 2026-10-29, so this refuses only a broken
    # install, or a wheel that will not load on this machine (an older
    # glibc), and says which rather than naming an extra that is gone.
    try:
        import OCP
    except ImportError as failure:
        raise ValueError(
            'solids and STEP/IGES need the OpenCASCADE kernel, which did '
            f'not import ({failure}) — reinstall it with pip install '
            '--force-reinstall cadquery-ocp') from None
    return OCP

shape

shape(geometry: Any, solid_id: int)

The solid as an OpenCASCADE shape, in the units the geometry holds its coordinates in.

A block or a cylinder is built and placed, a section or a hole is built from the blocks and cylinders it is made of, and a boolean or a pattern is computed from its operands' shapes. A boolean the kernel cannot compute raises ValueError naming the solid.

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required

Returns:

Type Description
TopoDS_Shape
Source code in src/visualdynamics/core/solids.py
def shape(geometry: Any, solid_id: int):
    """The solid as an OpenCASCADE shape, in the units the geometry holds
    its coordinates in.

    A block or a cylinder is built and placed, a section or a hole is
    built from the blocks and cylinders it is made of, and a boolean or a
    pattern is computed from its operands' shapes. A boolean the kernel
    cannot compute raises ValueError naming the solid.

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.

    Returns
    -------
    OCP.TopoDS.TopoDS_Shape
    """
    require_kernel()
    from OCP.BRepAlgoAPI import (
        BRepAlgoAPI_Common,
        BRepAlgoAPI_Cut,
        BRepAlgoAPI_Fuse,
    )

    item = solid(geometry, solid_id)
    if item.kind in BASIC:
        return _basic_shape(item)
    if item.kind in FLAT:
        return _flat_shape(item)
    if item.kind in SHAPED:
        # the uncut pieces made one, then the cut taken from them
        pieces = _pieces(geometry, item)
        kept = [_basic_shape(p) for cutting, p in pieces if not cutting]
        cut = [_basic_shape(p) for cutting, p in pieces if cutting]
        result = kept[0]
        for operand, algorithm in ([(k, BRepAlgoAPI_Fuse) for k in kept[1:]]
                                   + [(c, BRepAlgoAPI_Cut) for c in cut]):
            step = algorithm(result, operand)
            if not step.IsDone():
                raise ValueError(f'the {item.kind} of solid {item.id} failed '
                                 'in the kernel')
            result = step.Shape()
        return result
    if item.kind == PATTERN:
        from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
        from OCP.gp import gp_Trsf, gp_Vec

        originals = [shape(geometry, o) for o in item.operands]
        copies = []
        for offset in _offsets(item):
            trsf = gp_Trsf()
            trsf.SetTranslation(gp_Vec(*(float(v) for v in offset)))
            copies.extend(BRepBuilderAPI_Transform(original, trsf, True).Shape()
                          for original in originals)
        operands, algorithm = copies, BRepAlgoAPI_Fuse
    else:
        operands = [shape(geometry, o) for o in item.operands]
        algorithm = {'union': BRepAlgoAPI_Fuse, 'subtract': BRepAlgoAPI_Cut,
                     'intersect': BRepAlgoAPI_Common}[item.kind]
    result = operands[0]
    for operand in operands[1:]:
        step = algorithm(result, operand)
        if not step.IsDone():
            raise ValueError(f'the {item.kind} of solid {item.id} failed in '
                             'the kernel')
        result = step.Shape()
    return result

brick_count

brick_count(geometry: Any, ids, size: float) -> int

About how many bricks a mesh of these parts at size holds: their volume over the cube of the size, the volume read by sampling their box on a grid as fine as the size or _SAMPLES points along each axis, whichever is coarser. Kernel-free and quick, for the limit (MESH_LIMIT) and the pane's preview; a part thinner than the samples' spacing can be undercounted, so the meshers count the bricks they keep again.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required
size float

The brick size, as the geometry holds its coordinates.

required

Returns:

Type Description
int
Source code in src/visualdynamics/core/solids.py
def brick_count(geometry: Any, ids, size: float) -> int:
    """About how many bricks a mesh of these parts at `size` holds:
    their volume over the cube of the size, the volume read by sampling
    their box on a grid as fine as the size or `_SAMPLES` points along
    each axis, whichever is coarser. Kernel-free and quick, for the
    limit (`MESH_LIMIT`) and the pane's preview; a part thinner than the
    samples' spacing can be undercounted, so the meshers count the
    bricks they keep again.

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

    Returns
    -------
    int
    """
    ids = [int(i) for i in ids]
    low, high = zip(*(_box(geometry, i) for i in ids))
    low, high = np.min(low, axis=0), np.max(high, axis=0)
    span = high - low
    counts = [int(np.clip(round(w / float(size)), 1, _SAMPLES)) for w in span]
    rails = [low[k] + (np.arange(n) + 0.5) * span[k] / n for k, n in enumerate(counts)]
    at = np.stack(np.meshgrid(*rails, indexing='ij'), axis=-1).reshape(-1, 3)
    inside = np.zeros(len(at), dtype=bool)
    for i in ids:
        inside |= contains(geometry, i, at)
    held = float(inside.mean()) * float(np.prod(span))
    return round(held / float(size) ** 3)

volume

volume(geometry: Any, solid_id: int) -> float

The solid's volume, in the cube of the geometry's length unit (m³ once units are defined).

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required

Returns:

Type Description
float
Source code in src/visualdynamics/core/solids.py
def volume(geometry: Any, solid_id: int) -> float:
    """The solid's volume, in the cube of the geometry's length unit (m³
    once units are defined).

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.

    Returns
    -------
    float
    """
    require_kernel()
    from OCP.BRepGProp import BRepGProp
    from OCP.GProp import GProp_GProps

    props = GProp_GProps()
    BRepGProp.VolumeProperties_s(shape(geometry, solid_id), props)
    return float(props.Mass())

area

area(geometry: Any, solid_id: int) -> float

A flat solid's area, in the square of the geometry's length unit (m² once units are defined): what a plate or a disk reads as, having no volume.

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required

Returns:

Type Description
float
Source code in src/visualdynamics/core/solids.py
def area(geometry: Any, solid_id: int) -> float:
    """A flat solid's area, in the square of the geometry's length
    unit (m² once units are defined): what a plate or a disk reads as,
    having no volume.

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.

    Returns
    -------
    float
    """
    require_kernel()
    from OCP.BRepGProp import BRepGProp
    from OCP.GProp import GProp_GProps

    props = GProp_GProps()
    BRepGProp.SurfaceProperties_s(shape(geometry, solid_id), props)
    return float(props.Mass())

shape_triangles

shape_triangles(item, deflection: float = DEFLECTION) -> ndarray

The corners of the triangles OpenCASCADE tessellates a shape's faces into, with deflection a fraction of the shape's size. This is what draws a solid, and how the STEP importer reads a part.

Parameters:

Name Type Description Default
item TopoDS_Shape

The shape.

required
deflection float

How far a triangle may stand off the surface, as a fraction of the shape's size.

DEFLECTION

Returns:

Type Description
ndarray

(n, 3, 3): each triangle's three corners; none for a shape with nothing in it, such as a subtraction whose first solid lies inside the rest.

Source code in src/visualdynamics/core/solids.py
def shape_triangles(item, deflection: float = DEFLECTION) -> np.ndarray:
    """The corners of the triangles OpenCASCADE tessellates a shape's
    faces into, with `deflection` a fraction of the shape's size. This is
    what draws a solid, and how the STEP importer reads a part.

    Parameters
    ----------
    item : OCP.TopoDS.TopoDS_Shape
        The shape.
    deflection : float, optional
        How far a triangle may stand off the surface, as a fraction of
        the shape's size.

    Returns
    -------
    numpy.ndarray
        (n, 3, 3): each triangle's three corners; none for a shape with
        nothing in it, such as a subtraction whose first solid lies
        inside the rest.
    """
    from OCP.Bnd import Bnd_Box
    from OCP.BRep import BRep_Tool
    from OCP.BRepBndLib import BRepBndLib
    from OCP.BRepMesh import BRepMesh_IncrementalMesh
    from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED
    from OCP.TopExp import TopExp_Explorer
    from OCP.TopLoc import TopLoc_Location
    from OCP.TopoDS import TopoDS

    # the 7.9 binding suffixes a static that shares its name with a
    # method; 8.0 drops the suffix for this one and keeps it for the
    # rest (2026-09-13, Linux on 8.0.1 beside the Mac on 7.9.3)
    _as_face = getattr(TopoDS, 'Face_s', None) or TopoDS.Face

    box = Bnd_Box()
    BRepBndLib.Add_s(item, box)
    # an empty shape's box is void, and asking a void box for its
    # corners raises (Brandon, 2026-10-27: dragging a cone's inner
    # solid ahead of the outer one left nothing, and every redraw raised)
    if box.IsVoid():
        return np.empty((0, 3, 3))
    size = max(box.CornerMax().XYZ().Subtracted(
        box.CornerMin().XYZ()).Modulus(), 1e-9)
    BRepMesh_IncrementalMesh(item, size * deflection, False, 0.5, True)
    triangles: list[np.ndarray] = []
    walker = TopExp_Explorer(item, TopAbs_FACE)
    while walker.More():
        face = _as_face(walker.Current())
        location = TopLoc_Location()
        mesh = BRep_Tool.Triangulation_s(face, location)
        if mesh is not None:
            matrix = location.Transformation()
            points = np.asarray(
                [[mesh.Node(i + 1).X(), mesh.Node(i + 1).Y(),
                  mesh.Node(i + 1).Z()] for i in range(mesh.NbNodes())])
            rows = np.asarray(
                [[matrix.Value(r, c) for c in (1, 2, 3, 4)]
                 for r in (1, 2, 3)])
            points = points @ rows[:, :3].T + rows[:, 3]
            faces = np.asarray(
                [mesh.Triangle(i + 1).Get() for i in
                 range(mesh.NbTriangles())], dtype=np.int64) - 1
            if face.Orientation() == TopAbs_REVERSED:
                faces = faces[:, ::-1]
            triangles.append(points[faces])
        walker.Next()
    return (np.concatenate(triangles) if triangles
            else np.empty((0, 3, 3)))

contains

contains(geometry: Any, solid_id: int, points, ignore=()) -> ndarray

Which of points lie inside the solid, worked out from its recipe without the kernel: a block's box and a cylinder's radius and length, combined as its booleans say. A point on a face counts as inside.

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required
points array_like

(n, 3), as the geometry holds its coordinates.

required
ignore collection of int

Solids read as holding nothing: a cylinder subtracted is not cut, one unioned adds nothing. The mesher reads a part this way without the cylinders that run across its extrusion.

()

Returns:

Type Description
numpy.ndarray of bool
Source code in src/visualdynamics/core/solids.py
def contains(geometry: Any, solid_id: int, points, ignore=()) -> np.ndarray:
    """Which of `points` lie inside the solid, worked out from its recipe
    without the kernel: a block's box and a cylinder's radius and length,
    combined as its booleans say. A point on a face counts as inside.

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.
    points : array_like
        (n, 3), as the geometry holds its coordinates.
    ignore : collection of int, optional
        Solids read as holding nothing: a cylinder subtracted is not
        cut, one unioned adds nothing. The mesher reads a part this
        way without the cylinders that run across its extrusion.

    Returns
    -------
    numpy.ndarray of bool
    """
    points = np.asarray(points, dtype=float).reshape(-1, 3)
    item = solid(geometry, solid_id)
    if item.id in ignore:
        return np.zeros(len(points), dtype=bool)
    if item.kind in BASIC:
        return _basic_contains(item, points)
    if item.kind in SHAPED:
        return _pieces_contain(_pieces(geometry, item), points)
    if item.kind == PATTERN:
        # a section's or a hole's pieces worked out once, not once a copy:
        # a through hole's depth is asked of the whole table (`_through`),
        # and a pattern of 400 holes meshed three times slower than one of
        # cylinders that way
        tests = []
        for o in item.operands:
            if solid(geometry, o).kind in SHAPED:
                pieces = _pieces(geometry, solid(geometry, o))
                tests.append(lambda at, pieces=pieces: _pieces_contain(pieces, at))
            else:
                tests.append(lambda at, o=o: contains(geometry, o, at, ignore))
        return np.any([test(points - offset) for offset in _offsets(item)
                       for test in tests], axis=0)
    inside = [contains(geometry, o, points, ignore) for o in item.operands]
    if item.kind == 'union':
        return np.any(inside, axis=0)
    if item.kind == 'intersect':
        return np.all(inside, axis=0)
    return inside[0] & ~np.any(inside[1:], axis=0)

distance

distance(geometry: Any, solid_id: int, points, ignore=()) -> ndarray

The signed distance from each of points to the solid's surface, negative inside, worked out from its recipe: a block's and a cylinder's exact distances, a union the least of its operands', an intersection the greatest, a subtraction the greater of the first's and the negatives of the rest's. Exact for a block and a cylinder; for a boolean, exact outside and at the surface, and a bound inside, which is what a mesher needs: where the surface is and which way is out.

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required
points array_like

(n, 3), as the geometry holds its coordinates.

required
ignore collection of int

Solids read as holding nothing, as contains reads them.

()

Returns:

Type Description
ndarray

(n,), in the geometry's length unit.

Source code in src/visualdynamics/core/solids.py
def distance(geometry: Any, solid_id: int, points, ignore=()) -> np.ndarray:
    """The signed distance from each of `points` to the solid's surface,
    negative inside, worked out from its recipe: a block's and a
    cylinder's exact distances, a union the least of its operands', an
    intersection the greatest, a subtraction the greater of the first's
    and the negatives of the rest's. Exact for a block and a cylinder;
    for a boolean, exact outside and at the surface, and a bound
    inside, which is what a mesher needs: where the surface is and
    which way is out.

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.
    points : array_like
        (n, 3), as the geometry holds its coordinates.
    ignore : collection of int, optional
        Solids read as holding nothing, as `contains` reads them.

    Returns
    -------
    numpy.ndarray
        (n,), in the geometry's length unit.
    """
    points = np.asarray(points, dtype=float).reshape(-1, 3)
    item = solid(geometry, solid_id)
    if item.id in ignore:
        return np.full(len(points), np.inf)
    if item.kind in BASIC:
        return _basic_distance(item, points)
    if item.kind in SHAPED:
        pieces = _pieces(geometry, item)
        return _pieces_distance(pieces, points)
    if item.kind == PATTERN:
        tests = []
        for o in item.operands:
            if solid(geometry, o).kind in SHAPED:
                pieces = _pieces(geometry, solid(geometry, o))
                tests.append(lambda at, pieces=pieces: _pieces_distance(pieces, at))
            else:
                tests.append(lambda at, o=o: distance(geometry, o, at, ignore))
        return np.min([test(points - offset) for offset in _offsets(item)
                       for test in tests], axis=0)
    found = [distance(geometry, o, points, ignore) for o in item.operands]
    if item.kind == 'union':
        return np.min(found, axis=0)
    if item.kind == 'intersect':
        return np.max(found, axis=0)
    return np.maximum(found[0], -np.min(found[1:], axis=0))

distance_gradient

distance_gradient(geometry: Any, solid_id: int, points, ignore=()) -> tuple[ndarray, ndarray]

distance and its gradient together: the unit direction out of the material at each point, exact where the distance is smooth and one side's value on an edge or a corner. A boolean's gradient is the gradient of the operand that decided its distance, a cut operand's turned inward. Four distance evaluations a point by finite differences were most of a fit's projection cost (hexmesh, 2026-10-19).

Parameters:

Name Type Description Default
geometry Geometry

The geometry whose solids table holds it.

required
solid_id int

The solid, by id.

required
points array_like

(n, 3), as the geometry holds its coordinates.

required
ignore collection of int

Solids read as holding nothing, as contains reads them.

()

Returns:

Type Description
(ndarray, ndarray)

The distances, (n,), and the gradients, (n, 3).

Source code in src/visualdynamics/core/solids.py
def distance_gradient(geometry: Any, solid_id: int, points,
                      ignore=()) -> tuple[np.ndarray, np.ndarray]:
    """`distance` and its gradient together: the unit direction out of
    the material at each point, exact where the distance is smooth and
    one side's value on an edge or a corner. A boolean's gradient is
    the gradient of the operand that decided its distance, a cut
    operand's turned inward. Four distance evaluations a point by
    finite differences were most of a fit's projection cost (hexmesh,
    2026-10-19).

    Parameters
    ----------
    geometry : Geometry
        The geometry whose solids table holds it.
    solid_id : int
        The solid, by id.
    points : array_like
        (n, 3), as the geometry holds its coordinates.
    ignore : collection of int, optional
        Solids read as holding nothing, as `contains` reads them.

    Returns
    -------
    (numpy.ndarray, numpy.ndarray)
        The distances, (n,), and the gradients, (n, 3).
    """
    points = np.asarray(points, dtype=float).reshape(-1, 3)
    item = solid(geometry, solid_id)
    if item.id in ignore:
        return np.full(len(points), np.inf), np.zeros_like(points)
    if item.kind in BASIC:
        return _basic_gradient(item, points)
    if item.kind in SHAPED:
        return _pieces_gradient(_pieces(geometry, item), points)
    if item.kind == PATTERN:
        found = []
        for o in item.operands:
            inner = solid(geometry, o)
            for offset in _offsets(item):
                if inner.kind in SHAPED:
                    found.append(_pieces_gradient(_pieces(geometry, inner), points - offset))
                else:
                    found.append(distance_gradient(geometry, o, points - offset, ignore))
        return _least(found)
    found = [distance_gradient(geometry, o, points, ignore) for o in item.operands]
    if item.kind == 'union':
        return _least(found)
    if item.kind == 'intersect':
        return _greatest(found)
    dc, gc = _least(found[1:])
    return _greatest([found[0], (-dc, -gc)])

extrusion_axis

extrusion_axis(geometry: Any, ids) -> int

The axis (0, 1 or 2) the parts are extrusions along.

Every block must be square to the geometry's axes; a solid turned off them raises NotAnExtrusion naming it. The axis is the one most of the cylinders and rounded corners run along, the largest radius deciding a tie; the rest run across the extrusion and are pulled round from the grid rather than ringed. Blocks alone are an extrusion along any axis, and the axis along which their extent is thinnest is returned.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
int
Source code in src/visualdynamics/core/solids.py
def extrusion_axis(geometry: Any, ids) -> int:
    """The axis (0, 1 or 2) the parts are extrusions along.

    Every block must be square to the geometry's axes; a solid turned
    off them raises `NotAnExtrusion` naming it. The axis is the one most of
    the cylinders and rounded corners run along, the largest radius
    deciding a tie; the rest run across the extrusion and are pulled
    round from the grid rather than ringed. Blocks alone are an
    extrusion along any axis, and the axis along which their extent is
    thinnest is returned.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    int
    """
    leaves = [leaf for i in ids for leaf in _leaves(geometry, i)]
    for leaf in leaves:
        if leaf.kind in UNSWEPT:
            raise NotAnExtrusion(f'solid {leaf.id} is a {leaf.kind}, which no sweep makes: '
                                 'its bricks are fitted')
        axes = np.abs(_frame(leaf))
        if not np.allclose(np.sort(axes, axis=1), [[0, 0, 1]] * 3, atol=1e-9):
            raise NotAnExtrusion(
                f'solid {leaf.id} is turned off the geometry\'s axes: bricks '
                'mesh extrusions along X, Y or Z')
    # the cylinders along a second axis cannot be ringed, since a ring
    # is laid in the plane and swept (Brandon, 2026-10-18: a block with
    # a peanut cut along Z and a hole drilled in from the side along Y
    # was refused outright); the axis with the most round features
    # keeps its rings, and the rest are pulled round in their own plane
    along: dict[int, tuple[int, float]] = {}
    for leaf in leaves:
        if leaf.kind == 'cylinder' or leaf.corner > 0.0:
            axis = int(np.argmax(np.abs(_frame(leaf)[2])))
            count, widest = along.get(axis, (0, 0.0))
            along[axis] = (count + 1, max(widest, leaf.radius, leaf.corner))
    if along:
        return max(along, key=lambda axis: (along[axis], -axis))
    # blocks alone are an extrusion along any axis: the thinnest one
    low, high = zip(*(_box(geometry, i) for i in ids))
    return int(np.argmin(np.max(high, axis=0) - np.min(low, axis=0)))

mesh_size

mesh_size(geometry: Any, ids) -> float

The element size an extrusion is meshed at by default: half the thinnest feature, so every feature is two bricks across.

The thinnest feature is the shortest span between two flat faces (a cylinder's end along an axis counts as one), or the narrowest cylinder or rounded corner across (smallest_feature). With nothing to measure, the size is 1.0.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
float
Source code in src/visualdynamics/core/solids.py
def mesh_size(geometry: Any, ids) -> float:
    """The element size an extrusion is meshed at by default: half the
    thinnest feature, so every feature is two bricks across.

    The thinnest feature is the shortest span between two flat faces (a
    cylinder's end along an axis counts as one), or the narrowest
    cylinder or rounded corner across (`smallest_feature`). With
    nothing to measure, the size is 1.0.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    float
    """
    radius, wall = smallest_feature(geometry, ids)
    thinnest = min(2.0 * radius, wall)
    return 0.5 * thinnest if np.isfinite(thinnest) else 1.0

smallest_feature

smallest_feature(geometry: Any, ids) -> tuple[float, float]

(the smallest radius, the thinnest wall) of the parts, in the geometry's length unit: the narrowest cylinder or rounded corner, and the shortest span between two flat faces. Infinite where there is none.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
(float, float)
Source code in src/visualdynamics/core/solids.py
def smallest_feature(geometry: Any, ids) -> tuple[float, float]:
    """(the smallest radius, the thinnest wall) of the parts, in the
    geometry's length unit: the narrowest cylinder or rounded corner,
    and the shortest span between two flat faces. Infinite where there
    is none.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    (float, float)
    """
    ids = [int(i) for i in ids]
    leaves = [leaf for i in ids for leaf in _leaves(geometry, i)]
    # a cone's radius halfway up: at a point its tip has none, and a
    # mesh is sized by what it has to hold over most of its length
    radii = [leaf.radius if leaf.kind in ('cylinder', 'sphere')
             else 0.5 * (leaf.radius + _top(leaf)) if leaf.kind == 'cone' else leaf.corner
             for leaf in leaves if leaf.kind in BASIC[1:] or leaf.corner > 0.0]
    # the flat faces: every block's, and the caps of a cylinder along
    # an axis; a turned cylinder's caps are not walls (the grid's cuts
    # read them along the nearest axis, a line the grid needs and not a
    # wall, and Mesh Solids opened at 0.95 in on a block 3 in thick
    # with 1 in holes for it: Brandon, 2026-10-19)
    planes: list[set] = [set(), set(), set()]
    for leaf in leaves:
        center = np.asarray(leaf.center, dtype=float)
        frame = _frame(leaf)
        if leaf.kind == 'block':
            half = np.abs(frame.T) @ (np.asarray(leaf.widths, dtype=float) / 2.0)
            for k in range(3):
                planes[k].update((center[k] - half[k], center[k] + half[k]))
        elif leaf.kind != 'sphere' and np.isclose(np.abs(frame[2]).max(), 1.0):
            k = int(np.argmax(np.abs(frame[2])))
            planes[k].update((center[k] - leaf.length / 2.0, center[k] + leaf.length / 2.0))
    boxes = [_box(geometry, i) for i in ids]
    low = np.min([lo for lo, _hi in boxes], axis=0)
    high = np.max([hi for _lo, hi in boxes], axis=0)
    spans = [b - a for k in range(3)
             for a, b in pairwise(sorted(x for x in planes[k] if low[k] - 1e-9 <= x <= high[k] + 1e-9))
             if b - a > 1e-9 * (high[k] - low[k])]
    # a pipe's wall: the gap between the radii of two cylinders on one
    # axis (a wye's 0.3 in walls read as 6 in, its run's length,
    # without this)
    cylinders = [leaf for leaf in leaves if leaf.kind == 'cylinder']
    for a_index, a in enumerate(cylinders):
        axis_a = _frame(a)[2]
        for b in cylinders[a_index + 1:]:
            axis_b = _frame(b)[2]
            if abs(abs(float(axis_a @ axis_b)) - 1.0) > 1e-9:
                continue
            between = np.asarray(b.center, dtype=float) - np.asarray(a.center, dtype=float)
            off_axis = between - float(between @ axis_a) * axis_a
            if np.linalg.norm(off_axis) > 1e-9 * (a.radius + b.radius):
                continue
            gap = abs(a.radius - b.radius)
            if gap > 0.0:
                spans.append(gap)
    return (float(min(radii)) if radii else np.inf, float(min(spans)) if spans else np.inf)

refuse_parts_of_another

refuse_parts_of_another(geometry: Any, ids) -> None

Refuse a solid that is part of a boolean or a pattern: the one to mesh is the one it is part of.

Source code in src/visualdynamics/core/solids.py
def refuse_parts_of_another(geometry: Any, ids) -> None:
    """Refuse a solid that is part of a boolean or a pattern: the one
    to mesh is the one it is part of."""
    used = used_by(geometry)
    inside_another = [int(i) for i in ids if int(i) in used]
    if inside_another:
        raise ValueError(f'solid {inside_another[0]} is part of solid '
                         f'{used[inside_another[0]]}; mesh that one')

grid_cells

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

How many bricks the grid extrusion_mesh would lay for these parts at size holds, voids included: the count before any ownership is asked, so a pane can decide whether to preview a mesh before paying for it.

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

Raises:

Type Description
ValueError

As extrusion_mesh does, for parts or a size it would refuse.

Source code in src/visualdynamics/core/solids.py
def grid_cells(geometry: Any, ids, size: float, piece: bool = False) -> int:
    """How many bricks the grid `extrusion_mesh` would lay for these
    parts at `size` holds, voids included: the count before any
    ownership is asked, so a pane can decide whether to preview a mesh
    before paying for it.

    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

    Raises
    ------
    ValueError
        As `extrusion_mesh` does, for parts or a size it would refuse.
    """
    _axis, _order, lines = _grid_lines(geometry, ids, size, piece)
    nu, nv, nw = (len(line) for line in lines)
    return (nu - 1) * (nv - 1) * (nw - 1)

extrusion_mesh

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

The parts as eight-node bricks, 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). Parts that touch share their nodes there, or, with parts_apart, each keeps its own on the same grid, so the nodes of one lie on the other's and bolts can be merged there later.

The grid is cut at every flat face of every block along every axis, and each span is divided into the whole number of bricks nearest size, so flat faces fall on grid lines. A brick goes to the part whose recipe holds its center (contains), to the later part where parts overlap (a steel screw added over the insert it is threaded into makes those bricks steel), and to no part in a void. A hole, a boss or an insert is meshed round, not stair-stepped. About each circle with room, a ring mesh with its nodes evenly spaced on the circle replaces the grid. About a circle without room, or a rounded corner's arc, the nodes between its inside and the material beside it are moved radially onto it, except where the move would turn a brick inside out.

The extrusion runs along the axis most of the cylinders and rounded corners run along (extrusion_axis); a cylinder drilled across it is cut from the grid and its nodes pulled round in its own plane, without a ring, and where it meets a hole along the extrusion the bricks between the two walls are skewed. A solid turned off the axes is refused with the solid named, and so are a solid that is part of another, a size that is not positive, and a grid of more than MESH_LIMIT (2,000,000) bricks by its volume (brick_count) or a grid of more than GRID_LIMIT (10,000,000) 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. Where two overlap, the later takes the bricks.

required
size float

The element size aimed at, as the geometry holds its coordinates.

required
parts_apart bool

Each part keeps its own nodes where it touches another.

False
progress callable

Told (done, total) as the mesh goes: the grid's ownership, the plane and its rings, the rounding, the bricks (the window's bar reads it; anything it raises stops the mesh).

None
piece bool

The part is a union's operand meshed on its own (brick_mesh), which is otherwise refused as part of another.

False
cuts_from sequence of int

Solids whose flat faces the grid is cut at too, where they fall within the parts: the earlier pieces of a union, so the sweep ends where it meets them.

()

Returns:

Type Description
(Geometry, dict)

The bricks, in the geometry's length unit; and what was made: 'bricks' and 'nodes', the counts; 'axis' (0, 1 or 2); 'rounded', the number of nodes moved onto a circle; 'rings', the number of rings laid, one about each circle with room; and 'shortest', the shortest brick edge.

Source code in src/visualdynamics/core/solids.py
def extrusion_mesh(geometry: Any, ids, size: float,
                   parts_apart: bool = False,
                   progress: Any = None, piece: bool = False,
                   cuts_from=()) -> tuple[Any, dict]:
    """The parts as eight-node bricks, 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). Parts that touch share their nodes there,
    or, with `parts_apart`, each keeps its own on the same grid, so the
    nodes of one lie on the other's and bolts can be merged there later.

    The grid is cut at every flat face of every block along every axis,
    and each span is divided into the whole number of bricks nearest
    `size`, so flat faces fall on grid lines. A brick goes to the part
    whose recipe holds its center (`contains`), to the later part where
    parts overlap (a steel screw added over the insert it is threaded
    into makes those bricks steel), and to no part in a void. A hole, a
    boss or an insert is meshed round, not stair-stepped. About each
    circle with room, a ring mesh with its nodes evenly spaced on the
    circle replaces the grid. About a circle without room, or a rounded
    corner's arc, the nodes between its inside and the material beside
    it are moved radially onto it, except where the move would turn a
    brick inside out.

    The extrusion runs along the axis most of the cylinders and rounded
    corners run along (`extrusion_axis`); a cylinder drilled across it
    is cut from the grid and its nodes pulled round in its own plane,
    without a ring, and where it meets a hole along the extrusion the
    bricks between the two walls are skewed. A solid turned off the
    axes is refused with the solid named, and so are a solid that is
    part of another, a size that is not positive, and a grid of more
    than `MESH_LIMIT` (2,000,000) bricks by its volume (`brick_count`)
    or a grid of more than `GRID_LIMIT` (10,000,000) cells.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id; each a solid nothing else uses. Where two
        overlap, the later takes the bricks.
    size : float
        The element size aimed at, as the geometry holds its
        coordinates.
    parts_apart : bool, default False
        Each part keeps its own nodes where it touches another.
    progress : callable, optional
        Told ``(done, total)`` as the mesh goes: the grid's ownership,
        the plane and its rings, the rounding, the bricks (the window's
        bar reads it; anything it raises stops the mesh).
    piece : bool, default False
        The part is a union's operand meshed on its own (`brick_mesh`),
        which is otherwise refused as part of another.
    cuts_from : sequence of int, optional
        Solids whose flat faces the grid is cut at too, where they fall
        within the parts: the earlier pieces of a union, so the sweep
        ends where it meets them.

    Returns
    -------
    (Geometry, dict)
        The bricks, in the geometry's length unit; and what was made:
        'bricks' and 'nodes', the counts; 'axis' (0, 1 or 2); 'rounded',
        the number of nodes moved onto a circle; 'rings', the number of
        rings laid, one about each circle with room; and 'shortest', the
        shortest brick edge.
    """
    # parts_apart is for the BARC's box, channels and beam (2026-10-11):
    # meshed one at a time, their grids were a quarter inch from meeting.
    from .geometry import Geometry
    from .progress import Ticker

    ids = [int(i) for i in ids]
    axis, order, lines = _grid_lines(geometry, ids, size, piece, cuts_from)
    nu, nv, nw = (len(line) for line in lines)
    cells = (nu - 1) * (nv - 1) * (nw - 1)
    _within_limits(geometry, ids, size, cells)
    ticker = Ticker(progress)
    ticker.add(4)
    # the rings are laid as if the cylinders across the extrusion were
    # not there, since a ring's rectangle must hold nothing but its own
    # outline; the bricks are then owned by what truly holds them
    across = _cross_cylinders(geometry, ids, axis)
    xyz, conn, groups, made = _mesh_grid(
        lines, order, size, lambda points: _owners(geometry, ids, points),
        _circles(geometry, ids, axis), ticker,
        held_plane=(lambda points: _owners(geometry, ids, points, across))
        if across else None,
        cross=_cross_circles(geometry, ids, axis))
    if not len(conn):
        raise ValueError('the parts hold nothing to mesh')
    if parts_apart:
        # a node for each part that uses it: one grid, no node shared
        stride = int(groups.max()) + 1
        key = (conn.ravel() - 1) * stride + np.repeat(groups, 8)
        kept, inverse = np.unique(key, return_inverse=True)
        xyz, conn = xyz[kept // stride], inverse.reshape(-1, 8) + 1
    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(xyz) + 1), node_xyz=xyz,
                    elem_conn=list(conn), elem_type=[115] * len(conn),
                    elem_group=[int(g) for g in groups], group_id=present,
                    group_name=names, length_unit=geometry.length_unit)
    return part, {'bricks': len(conn), 'nodes': len(xyz), 'axis': axis, **made}

brick_mesh

brick_mesh(geometry: Any, ids, size: float, parts_apart: bool = False, progress: Any = None, sizes: Any = None) -> tuple[Any, dict]

The parts as eight-node bricks: extrusion_mesh where they are an extrusion along X, Y or Z, with rings about the holes; where they are not and are a union, its operands one at a time (pieces), each swept where it is an extrusion and fitted where it is not, at its own size, joined where their nodes coincide and tied at the seams where they do not; and hexmesh.overlay_mesh, a grid fitted to the shape, for a lone part that is no extrusion. What Mesh Solids runs for Hexes.

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 aimed at, as the geometry holds its coordinates.

required
parts_apart bool

Each part keeps its own nodes where it touches another; the extrusion mesher's option, not the fitted grid's.

False
progress callable

Told (done, total) as the mesh goes.

None
sizes mapping of int to float

A size per piece, by solid id, for a union meshed in pieces; a piece without one takes size.

None

Returns:

Type Description
(Geometry, dict)

The bricks and what was made, as the mesher that ran says it, with 'method': 'extrusion', 'overlay' or 'pieces'. In pieces, 'pieces' lists each piece's readings with its 'name', 'method', 'size' and 'dropped' (its bricks inside the pieces before it), 'seam_shared' counts the nodes the pieces share at their seams, 'ties' the nodes joined there by rigid links and 'seams' the mated surfaces that join them (part.mates).

Source code in src/visualdynamics/core/solids.py
def brick_mesh(geometry: Any, ids, size: float, parts_apart: bool = False,
               progress: Any = None, sizes: Any = None) -> tuple[Any, dict]:
    """The parts as eight-node bricks: `extrusion_mesh` where they are
    an extrusion along X, Y or Z, with rings about the holes; where
    they are not and are a union, its operands one at a time (`pieces`),
    each swept where it is an extrusion and fitted where it is not, at
    its own size, joined where their nodes coincide and tied at the
    seams where they do not; and `hexmesh.overlay_mesh`, a grid fitted
    to the shape, for a lone part that is no extrusion. What Mesh
    Solids runs for Hexes.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id; each a solid nothing else uses.
    size : float
        The element size aimed at, as the geometry holds its
        coordinates.
    parts_apart : bool, default False
        Each part keeps its own nodes where it touches another; the
        extrusion mesher's option, not the fitted grid's.
    progress : callable, optional
        Told ``(done, total)`` as the mesh goes.
    sizes : mapping of int to float, optional
        A size per piece, by solid id, for a union meshed in pieces;
        a piece without one takes `size`.

    Returns
    -------
    (Geometry, dict)
        The bricks and what was made, as the mesher that ran says it,
        with 'method': 'extrusion', 'overlay' or 'pieces'. In pieces,
        'pieces' lists each piece's readings with its 'name', 'method',
        'size' and 'dropped' (its bricks inside the pieces before it),
        'seam_shared' counts the nodes the pieces share at their seams,
        'ties' the nodes joined there by rigid links and 'seams' the
        mated surfaces that join them (`part.mates`).
    """
    ids = [int(i) for i in ids]
    try:
        part, made = extrusion_mesh(geometry, ids, size, parts_apart, progress=progress)
    except NotAnExtrusion:
        from .hexmesh import overlay_mesh

        found = piece_cuts(geometry, ids)
        if len(found) > 1:
            return _pieces_mesh(geometry, found, size, sizes, progress)
        if len(ids) == 1:
            # one block, cylinder or section turned off the axes: swept
            # along its own, nothing to run into (2026-10-20)
            try:
                part, made = turned_sweep(geometry, ids[0], size, progress=progress)
                return part, {**made, 'method': 'extrusion'}
            except NotAnExtrusion:
                pass
        part, made = overlay_mesh(geometry, ids, size, progress=progress)
        return part, {**made, 'method': 'overlay'}
    return part, {**made, 'method': 'extrusion'}

piece_cuts

piece_cuts(geometry: Any, ids) -> list[tuple[int, tuple[int, ...]]]

The pieces a brick mesh is made in, each with the solids cut from it: (piece, cuts).

A lone union gives up its operands, and a union among them gives up its own, all the way down, so a part unioned again with something keeps its pieces (Brandon, 2026-10-20: a table unioned with a sphere was fitted whole, its legs and rod with it). A subtraction whose first operand holds a union is split the same way, each member less what the subtraction takes: (A + B) - C is A - C and B - C. Any other solid, and a subtraction of no union, is one piece; so are parts given together, each its own.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
list of (int, tuple of int)
Source code in src/visualdynamics/core/solids.py
def piece_cuts(geometry: Any, ids) -> list[tuple[int, tuple[int, ...]]]:
    """The pieces a brick mesh is made in, each with the solids cut
    from it: (piece, cuts).

    A lone union gives up its operands, and a union among them gives up
    its own, all the way down, so a part unioned again with something
    keeps its pieces (Brandon, 2026-10-20: a table unioned with a
    sphere was fitted whole, its legs and rod with it). A subtraction
    whose first operand holds a union is split the same way, each
    member less what the subtraction takes: (A + B) - C is A - C and
    B - C. Any other solid, and a subtraction of no union, is one piece;
    so are parts given together, each its own.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    list of (int, tuple of int)
    """
    ids = [int(i) for i in ids]
    if len(ids) != 1:
        return [(i, ()) for i in ids]

    def walk(i: int, cuts: tuple[int, ...]) -> list[tuple[int, tuple[int, ...]]]:
        item = solid(geometry, i)
        if item.kind == 'union':
            return [found for o in item.operands for found in walk(int(o), cuts)]
        if item.kind == 'subtract':
            first, *rest = (int(o) for o in item.operands)
            inner = walk(first, (*cuts, *rest))
            if len(inner) > 1:
                return inner
        return [(i, cuts)]

    found = walk(ids[0], ())
    return found if len(found) > 1 else [(ids[0], ())]

meshed_as

meshed_as(geometry: Any, piece: int, cuts=())

The id of the solid a piece is meshed as, while the block runs: the piece itself, or the subtraction of cuts from it, made for the while and taken off after, so a reading of the piece (its likely size, whether a fit is likely to fold) is a reading of what is meshed, a hole included.

Source code in src/visualdynamics/core/solids.py
@contextlib.contextmanager
def meshed_as(geometry: Any, piece: int, cuts=()):
    """The id of the solid a piece is meshed as, while the block runs:
    the piece itself, or the subtraction of `cuts` from it, made for the
    while and taken off after, so a reading of the piece (its likely
    size, whether a fit is likely to fold) is a reading of what is
    meshed, a hole included."""
    found = [(int(piece), tuple(int(c) for c in cuts))]
    made = _cut_pieces(geometry, found)
    try:
        yield made[0]
    finally:
        _drop_cut_pieces(geometry, found, made)

pieces

pieces(geometry: Any, ids) -> list[int]

The pieces a brick mesh is made in when the parts together are not an extrusion (piece_cuts), without what is cut from them.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required

Returns:

Type Description
list of int
Source code in src/visualdynamics/core/solids.py
def pieces(geometry: Any, ids) -> list[int]:
    """The pieces a brick mesh is made in when the parts together are
    not an extrusion (`piece_cuts`), without what is cut from them.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.

    Returns
    -------
    list of int
    """
    return [piece for piece, _cuts in piece_cuts(geometry, ids)]

piece_name

piece_name(geometry: Any, solid_id: int) -> str

The name a piece's element group takes: the solid's name, or its kind and id.

Source code in src/visualdynamics/core/solids.py
def piece_name(geometry: Any, solid_id: int) -> str:
    """The name a piece's element group takes: the solid's name, or
    its kind and id."""
    item = solid(geometry, solid_id)
    return item.name.strip() or f'{item.kind.title()} {item.id}'

piece_plan

piece_plan(geometry: Any, ids, size: float, sizes: Any = None) -> list[dict]

How brick_mesh would mesh these parts in pieces: for each piece its id, 'name', 'method' ('extrusion' for a sweep, 'overlay' for a fit), 'size', 'cells', the grid's cells at that size (voids included), and 'bricks', about how many the mesh holds (brick_count), and 'cuts', the solids cut from the piece. The pieces are piece_cuts', as deep as unions go. One entry, for the parts together, where they are one extrusion or no union to take apart; the pane reads it for its preview budget and its sizes per piece.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
ids sequence of int

The parts, by id.

required
size float

The size for every piece not given its own, as the geometry holds its coordinates.

required
sizes mapping of int to float

A size per piece, by solid id.

None

Returns:

Type Description
list of dict
Source code in src/visualdynamics/core/solids.py
def piece_plan(geometry: Any, ids, size: float, sizes: Any = None) -> list[dict]:
    """How `brick_mesh` would mesh these parts in pieces: for each
    piece its id, 'name', 'method' ('extrusion' for a sweep, 'overlay'
    for a fit), 'size', 'cells', the grid's cells at that size (voids
    included), and 'bricks', about how many the mesh holds
    (`brick_count`), and 'cuts', the solids cut from the piece. The
    pieces are `piece_cuts`', as deep as unions go.
    One entry, for the parts together, where they are one extrusion or
    no union to take apart; the pane reads it for its preview budget
    and its sizes per piece.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    ids : sequence of int
        The parts, by id.
    size : float
        The size for every piece not given its own, as the geometry
        holds its coordinates.
    sizes : mapping of int to float, optional
        A size per piece, by solid id.

    Returns
    -------
    list of dict
    """
    ids = [int(i) for i in ids]
    refuse_parts_of_another(geometry, ids)
    sizes = {int(k): float(v) for k, v in (sizes or {}).items()}
    found, made = [], []
    try:
        extrusion_axis(geometry, ids)
        parts = [ids]
    except NotAnExtrusion:
        found = piece_cuts(geometry, ids)
        if len(found) == 1:
            parts = [ids]
        else:
            # a piece with cuts is planned as the subtraction it is
            # meshed as, its size kept under the piece's own id
            made = _cut_pieces(geometry, found)
            sizes.update({m: sizes[p] for (p, _c), m in zip(found, made) if p in sizes})
            parts = [[m] for m in made]
    try:
        return _plan(geometry, parts, size, sizes, found, made)
    finally:
        if made:
            _drop_cut_pieces(geometry, found, made)

turned_sweep

turned_sweep(geometry: Any, piece: int, size: float, earlier=(), progress: Any = None) -> tuple[Any, dict]

A block, cylinder or section turned off the geometry's axes, swept along its own axis: meshed as an extrusion unturned at the origin (extrusion_mesh), its end where it runs into the pieces before it put on their surface, and turned and placed back.

The end goes on the earlier pieces' surface line by line: each line of nodes along the sweep is cut where it leaves them and its nodes spread evenly between there and its free end, so a rod entering a block at a slant is sheared over its length, its last layer on the block's face. A block or a section is a grid along all three of its own axes, so the sweep runs along whichever of them meets the earlier pieces at one end; a cylinder's rings run along its own Z. The bricks' quality is the worst scaled Jacobian (hexmesh), and a brick folded by the shear refuses the sweep, as does a piece that meets the earlier pieces other than at one end (NotAnExtrusion, saying why): the piece is then fitted.

Parameters:

Name Type Description Default
geometry Geometry

Whose solids.

required
piece int

The piece, by id: one primitive nothing else uses.

required
size float

The brick size, as the geometry holds its coordinates.

required
earlier sequence of int

The pieces meshed before it, which it runs into.

()
progress callable

Told (done, total).

None

Returns:

Type Description
(Geometry, dict)

The bricks, in the geometry's frame, and what extrusion_mesh says, with 'axis' None (its own), 'turned' True, 'cut', how many lines of nodes end on the earlier pieces, 'quality' and 'poor' (worst scaled Jacobian, and how many under 0.3).

Source code in src/visualdynamics/core/solids.py
def turned_sweep(geometry: Any, piece: int, size: float, earlier=(),
                 progress: Any = None) -> tuple[Any, dict]:
    """A block, cylinder or section turned off the geometry's axes,
    swept along its own axis: meshed as an extrusion unturned at the
    origin (`extrusion_mesh`), its end where it runs into the pieces
    before it put on their surface, and turned and placed back.

    The end goes on the earlier pieces' surface line by line: each line
    of nodes along the sweep is cut where it leaves them and its nodes
    spread evenly between there and its free end, so a rod entering a
    block at a slant is sheared over its length, its last layer on the
    block's face. A block or a section is a grid along all three of its
    own axes, so the sweep runs along whichever of them meets the
    earlier pieces at one end; a cylinder's rings run along its own Z.
    The bricks' quality is the worst scaled Jacobian (`hexmesh`), and a
    brick folded by the shear refuses the sweep, as does a piece that
    meets the earlier pieces other than at one end (NotAnExtrusion,
    saying why): the piece is then fitted.

    Parameters
    ----------
    geometry : Geometry
        Whose solids.
    piece : int
        The piece, by id: one primitive nothing else uses.
    size : float
        The brick size, as the geometry holds its coordinates.
    earlier : sequence of int
        The pieces meshed before it, which it runs into.
    progress : callable, optional
        Told ``(done, total)``.

    Returns
    -------
    (Geometry, dict)
        The bricks, in the geometry's frame, and what `extrusion_mesh`
        says, with 'axis' None (its own), 'turned' True, 'cut', how
        many lines of nodes end on the earlier pieces, 'quality' and
        'poor' (worst scaled Jacobian, and how many under 0.3).
    """
    # Brandon, 2026-10-20: the mesh on the tilted rod did not look
    # aligned with it; sweep each piece along its own axis, its end on
    # the face it meets, and check the end bricks' quality
    from .hexmesh import _scaled_jacobians

    item = _turned_primitive(geometry, piece)
    scratch = _unturned(geometry, item)
    part, made = extrusion_mesh(scratch, [item.id], size, progress=progress, piece=True)
    local = part.node_xyz.copy()
    tolerance = 1e-6 * float(np.linalg.norm(np.ptp(local, axis=0)) or 1.0)
    rows = {int(n): k for k, n in enumerate(part.node_id)}
    hexes = np.array([[rows[int(n)] for n in conn] for conn in part.elem_conn], dtype=np.int64)
    cut = 0
    if earlier:
        tried = [2] if item.kind == 'cylinder' else [2, 0, 1]
        reasons = []
        for axis in tried:
            try:
                local, cut = _end_on_earlier(geometry, item, local, axis, earlier, tolerance)
                break
            except NotAnExtrusion as why:
                reasons.append(str(why))
        else:
            raise NotAnExtrusion(reasons[0])
    part.node_xyz = np.asarray(item.center, dtype=float) + local @ _frame(item)
    quality = _scaled_jacobians(part.node_xyz, hexes).min(axis=1)
    # a guard rather than a case: each line keeps its order along the
    # sweep and every brick its cross-section, so the shear lowers a
    # brick's quality and cannot turn it inside out (no test can fold
    # one, 2026-10-20); a later change to the spreading could
    folded = int((quality <= 0.0).sum())
    if folded:
        raise NotAnExtrusion(f'{piece_name(geometry, piece)}: {folded:,} bricks fold where '
                             'its end meets the pieces before it')
    return part, {**made, 'axis': None, 'turned': True, 'cut': cut,
                  'quality': float(quality.min()), 'poor': int((quality < 0.3).sum())}

meshed_elements

meshed_elements(geometry: Any, names) -> ndarray

The ids of the elements Mesh Solids makes, bricks, tetrahedra and shells, in the element groups named names: what meshing those parts again replaces (Project.mesh_solids, and the pane's reading before Mesh is pressed).

Parameters:

Name Type Description Default
geometry Geometry

Whose elements.

required
names iterable of str

The element groups' names; an empty one names none.

required

Returns:

Type Description
numpy.ndarray of int
Source code in src/visualdynamics/core/solids.py
def meshed_elements(geometry: Any, names) -> np.ndarray:
    """The ids of the elements Mesh Solids makes, bricks, tetrahedra
    and shells, in the element groups named `names`: what meshing
    those parts again replaces (`Project.mesh_solids`, and the pane's
    reading before Mesh is pressed).

    Parameters
    ----------
    geometry : Geometry
        Whose elements.
    names : iterable of str
        The element groups' names; an empty one names none.

    Returns
    -------
    numpy.ndarray of int
    """
    names = {name for name in names if name}
    wanted = [int(g) for g, name in zip(geometry.group_id, geometry.group_name)
              if name in names]
    mask = (np.isin(np.asarray(geometry.elem_group, dtype=int), wanted)
            & np.isin(np.asarray(geometry.elem_type, dtype=int), MESHED_TYPES))
    return np.asarray(geometry.elem_id, dtype=int)[mask]