VNF meshes

VNF (vertices+faces) surface structure and grid meshing (BOSL2 vnf.scad).

class pybosl2.vnf.VNF(vertices=None, faces=None)[source]

Bases: object

A VNF surface: vertices (3-D points) plus faces (index polygons into vertices).

Renders to PythonSCAD’s native polyhedron via polyhedron(). Build one from a rectangular grid of sample points with vertex_array(), merge several with union(), or mesh a scalar field with from_field() and combine metaball primitives with from_metaballs().

Parameters:
vertices : list[list[float]] | None

list of [x, y, z] points

faces : list[list[int]] | None

list of index lists (each polygon into vertices)

Examples

Meshing a bumpy grid of sample points into a surface and rendering it as a polyhedron:

import math
from pybosl2 import Path3D, VNF

grid = [Path3D([[x, y, 4 * math.sin(x / 6) * math.cos(y / 6)] for y in range(0, 60, 4)])
        for x in range(0, 60, 4)]
VNF.vertex_array(grid).polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh

bounds()[source]

Axis-aligned Bounds3D of the VNF.

Return type:

Bounds3D

reverse()[source]

Return a copy with every face wound the other way (flips the surface normals).

Return type:

VNF

is_watertight()[source]

Whether this mesh is a closed manifold: every undirected edge shared by exactly two faces.

A watertight mesh bounds a solid, so it can be exported, unioned or measured; an open one cannot, and a slicer will either refuse it or repair it into something the caller did not ask for. The test is on topology alone – it reads faces and never vertices – so it is cheap and says nothing about self-intersection or winding.

Returns:

True if every edge has exactly two incident faces, False for an open or empty mesh.

Return type:

bool

Examples

A cube built as a closed grid is watertight; one open face is not:

>>> from pybosl2 import VNF
>>> VNF([[0, 0, 0], [1, 0, 0], [0, 1, 0]], [[0, 1, 2]]).is_watertight()
False
volume()[source]

Signed enclosed volume (BOSL2 vnf_volume()); negative when the faces wind inward.

Used to detect and fix inverted meshes (a swept/skinned surface whose winding came out inside-out): vnf if vnf.volume() >= 0 else vnf.reverse().

Return type:

float

classmethod union(vnfs)[source]

Merge a list of VNFs into one, offsetting each VNF’s face indices (BOSL2 vnf_join()).

Parameters:
vnfs : list[VNF]

The meshes to combine.

Return type:

VNF

classmethod join(vnfs)[source]

Merge multiple VNFs into a single consolidated VNF with shared vertices.

Each input VNF’s vertices and faces are copied into a combined vertex array, with face indices offset appropriately. No deduplication is performed.

Parameters:
vnfs : list[VNF]

A list of VNF objects to merge.

Returns:

A new VNF containing all vertices and faces from the inputs.

Return type:

VNF

Examples: .. pythonscad-example:

from pybosl2 import Path3D, VNF

a = VNF.vertex_array([Path3D([[0, 0, 0], [1, 0, 0]]), Path3D([[0, 1, 0], [1, 1, 0]])])
b = VNF.vertex_array([Path3D([[0, 0, 1], [1, 0, 1]]), Path3D([[0, 1, 1], [1, 1, 1]])])
VNF.join([a, b]).polyhedron().show()
halfspace(plane, keep=True, closed=True)[source]

Clip a VNF to one side of a plane, optionally closing the cut face.

A plane is defined as [A, B, C, D] for A*x + B*y + C*z = D. If keep is True, the positive halfspace (A*x + B*y + C*z > D) is retained. If keep is False, the negative halfspace is retained.

Parameters:
plane : Sequence[float]

Plane equation [A, B, C, D].

keep : bool

If True, keep the positive halfspace. Defaults to True.

closed : bool

If True, triangulate and close the cut face. Defaults to True.

Returns:

A new VNF containing only the requested halfspace.

Raises:

ValueError – If plane does not have exactly 4 elements.

Return type:

VNF

Examples: .. pythonscad-example:

import numpy as np
from pybosl2 import VNF, Bounds3D

cube_vnf = VNF.from_field(
    lambda p: 5 - np.max(np.abs(p), axis=1),
    0, Bounds3D(-10,-10,-10,10,10,10,20,20,20), voxel_size=1
)
cut = cube_vnf.halfspace([0, 0, 1, 0], keep=True, closed=True)
cut.polyhedron().show()
slice(plane, closed=True)[source]

Slice a VNF into two VNFs along a plane, closing both cut faces.

Returns (vnf_above, vnf_below) where vnf_above is the positive halfspace and vnf_below is the negative halfspace.

Parameters:
plane : Sequence[float]

Plane equation [A, B, C, D] for A*x + B*y + C*z = D.

closed : bool

If True, close both cut faces. Defaults to True.

Returns:

A (above, below) tuple of VNF objects.

Return type:

tuple[‘VNF’, ‘VNF’]

Examples: .. pythonscad-example:

import numpy as np
from pybosl2 import VNF, Bounds3D

cube_vnf = VNF.from_field(
    lambda p: 5 - np.max(np.abs(p), axis=1),
    0, Bounds3D(-10,-10,-10,10,10,10,20,20,20), voxel_size=1
)
above, below = cube_vnf.slice([0, 0, 1, 0], closed=True)
above.polyhedron().show()
export(path, *, file_format=None, check=True)[source]

Write this mesh to a file (SPEC S-53).

Pure Python and numpy – no CAD runtime – so a mesh built with no kernel present can be saved with none present either (SPEC S-54, A-2).

Parameters:
path : str | os.PathLike[str]

destination file. Its suffix picks the format – .stl, .obj, .off, .ply – unless file_format overrides it.

file_format : str | None

explicit format name ("stl", "stla" for ASCII STL, "obj", "off", "ply").

check : bool

validate the mesh first and refuse to write one that is open or wound inside out (SPEC S-55). False for a surface that is open on purpose.

Returns:

The path written.

Raises:

Bosl2ValueError – If the format is unknown, or check is on and the mesh is not a closed, outward-wound solid.

Return type:

FilePath

Examples

from pybosl2 import Path2D

bar = Path2D([[-5, -5], [5, -5], [5, 5], [-5, 5]], closed=True).linear_sweep(height=20)
bar.vnf().export("bar.stl")
bar.show()
Loading 3-D preview…

⬇ Download STL mesh

classmethod from_solid(solid)[source]

Mesh solid into a VNF (SPEC C-8).

The way back across the boundary polyhedron() crosses the other way, so anything the library can build can also be measured, joined or exported without the caller reaching for a native handle. Faces come back wound the way the native layer wants them and are reversed on the way in, matching the convention volume() and polyhedron() assume: counter-clockwise seen from outside, positive volume for a solid.

Parameters:
solid : Solid

any 3-D shape on any backend.

Returns:

The mesh, as an ordinary VNF.

Raises:

Bosl2ValueError – If the solid produced no geometry to mesh.

Return type:

VNF

Examples

from pybosl2 import cuboid, VNF

mesh = VNF.from_solid(cuboid([20, 20, 20]))
print(mesh.volume())      # 8000.0
mesh.polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh

classmethod vertex_array(points, caps=None, col_wrap=False, row_wrap=False, reverse=False, style=VNFStyle.DEFAULT)[source]

Build a VNF from a rectangular grid of 3-D points (BOSL2 vnf_vertex_array()).

Each grid cell becomes triangles (or a quad) chosen by style: “default”, “alt”, “min_edge”, “min_area”, “convex”, “concave”, “quincunx”, “quad”, “flip1”, “flip2”. col_wrap/row_wrap close the grid into a tube/torus; caps closes the column-wrapped ends with CapType or CapSpec styles (see CapsSpec). reverse flips face winding. Degenerate (zero-area) faces are dropped.

Parameters:
points : Sequence[Path3D]

The grid, one Path3D per row (SPEC C-7a). A grid is a sequence of rows and a row is an ordered set of points, so each row is a path.

caps : CapsSpec | None

Cap specification for both ends: a single CapType, CapSpec, or a two-element pair [cap_start, cap_end]. Pass None (the default) for no caps.

col_wrap : bool

Close the column direction into a tube.

row_wrap : bool

Close the row direction into a torus.

reverse : bool

Flip face winding.

style : VNFStyle | VnfStyle

Triangulation method.

Return type:

VNF

classmethod tri_array(points, caps=False, cap1=None, cap2=None, col_wrap=False, row_wrap=False, reverse=False, limit_bunching=True)[source]

Build a VNF from an array of rows whose lengths may differ (BOSL2 vnf_tri_array()).

Triangulates between adjacent rows by repeatedly adding the shortest new edge, so it meshes triangular / irregular point arrays (what the degenerate bezier patches produce).

Parameters:
points : Sequence[Path3D]

The rows, one Path3D each; unlike vertex_array() they may differ in length (SPEC C-7a).

caps : bool

Close both open ends.

cap1 : bool | None

Close the first end (overrides caps).

cap2 : bool | None

Close the last end (overrides caps).

col_wrap : bool

Wrap each row back to its own first point.

row_wrap : bool

Wrap the last row back to the first.

reverse : bool

Flip face winding.

limit_bunching : bool

Limit how many triangles may fan from one vertex.

Returns:

The triangulated mesh.

Raises:

Bosl2ValueError – If points is not a sequence of Path3D, or caps are combined with row_wrap.

Return type:

VNF

polyhedron()[source]

Build this VNF on the active backend.

A VNF winds its faces counter-clockwise seen from outside (so volume() is positive for a solid); the native polyhedron() wants them the other way round, so each face is reversed on the way out. Handing them over as-is builds the solid inside out – it still looks right on its own, but every union or difference with it then does the opposite of what it should.

This dispatches through the backend rather than calling the native directly, so a convex mesh builds on either. The SDF backend’s polyhedron is the intersection of its face half-spaces, which can only be convex, and it refuses a mesh that is not – so a concave VNF says so here instead of quietly coming back as its own hull (SPEC B-4, B-9).

Returns:

The solid, built by whichever backend is active.

Raises:

ValueError – If this VNF has no vertices or no faces.

Return type:

Solid

geometry()[source]

Return the VNF as native polyhedron geometry.

Return type:

Any

classmethod from_field(f, isovalue, bounding_box=None, voxel_size=None, voxel_count=None, closed=True, reverse=False, exact_bounds=False)[source]

Mesh a scalar field into a VNF via marching cubes.

The solid is the region where f >= isovalue.

Parameters:
f : np.ndarray | Path3D | Callable[[np.ndarray], np.ndarray] | Callable[[Path3D], np.ndarray]

A Path3D, a 3-D numpy array, a (N,3) (N,) callable, or a (:class:`~pybosl2.path3d.Path3D`) (N,) callable.

isovalue : float

Scalar threshold.

bounding_box : Bounds3D | float | Sequence[float] | Sequence[Sequence[float]] | None

A Bounds3D or None (auto-computed from array shape when f is an array).

voxel_size : float | None

Isotropic voxel size.

voxel_count : int | None

Approximate total voxel count (ignored if voxel_size given).

closed : bool

If True, pad field so mesh closes at bounding-box faces.

reverse : bool

If True, reverse inside/outside sense.

exact_bounds : bool

If True, use bounding_box exactly.

Returns:

A VNF.

Raises:

Bosl2NotImplementedError – If isovalue is a tuple range; only scalar thresholds are built.

Return type:

VNF

Examples: .. pythonscad-example:

import numpy as np
from pybosl2 import VNF, Bounds3D

def field(p: np.ndarray) -> np.ndarray:
    x, y, z = p[:, 0], p[:, 1], p[:, 2]
    return np.asarray(20 / np.sqrt(x * x + y * y + z * z) + 3 * np.sin(x / 3))
VNF.from_field(
    field, 1,
    Bounds3D(-30, -30, -30, 30, 30, 30, 60, 60, 60),
    voxel_size=2,
).polyhedron().show()
classmethod from_metaballs(spec, bounding_box, voxel_size=None, voxel_count=None, isovalue=1, closed=True, exact_bounds=False)[source]

Mesh transformed metaball primitives into a blobby VNF.

Parameters:
spec : list[MetaballSpec]

A list of MetaballSpec entries, each holding a transform (4×4 matrix or Point position) and a Metaball.

bounding_box : Bounds3D | float | Sequence[float] | Sequence[Sequence[float]]

A Bounds3D.

voxel_size : float | None

Isotropic voxel size.

voxel_count : int | None

Approximate total voxel count.

isovalue : float

Field threshold.

closed : bool

Close mesh at bounding-box faces.

exact_bounds : bool

Use bounding_box exactly.

Returns:

A VNF.

Return type:

VNF

Examples: .. pythonscad-example:

from pybosl2.isosurface import MetaballSpec, mb_sphere
from pybosl2 import VNF, Bounds3D

spec = [
    MetaballSpec([-14, 0, 0], mb_sphere(12)),
    MetaballSpec([14, 0, 0], mb_sphere(12)),
]
VNF.from_metaballs(
    spec,
    Bounds3D(-40, -20, -20, 40, 20, 20, 80, 40, 40),
    voxel_size=2,
).polyhedron().show()
classmethod from_skin(profiles, slices, refine=1.0, method=SkinMethod.DIRECT, sampling=None, caps='butt', closed=False, style=VNFStyle.MIN_EDGE, z=None)[source]

Blend a stack of 2-D/3-D profiles into a skinned surface, returning a VNF or Bosl2Solid.

Consecutive profiles are connected vertex-to-vertex; slices extra interpolated profiles are inserted between each pair to smooth the transition.

Parameters:
profiles : Sequence[Path2D | Path3D]

list of >= 2 closed profiles (each a list of points). If 2-D, give matching z.

slices : int

number of interpolated profiles inserted between each pair (int or per-gap list)

refine : float

subdivide every profile by this factor before skinning (default 1)

method : SkinMethod

“direct” (connect vertex i to vertex i) or “reindex” (rotate each profile to best-align with the previous).

sampling : SamplingType | None

“length” or “segment” resampling (default “length”)

caps : CapsSpec

cap the ends; supports decorative cap types

closed : bool

the stack loops back to the first profile (default False)

style : VNFStyle

vnf_vertex_array quad-subdivision style

z : Sequence[float] | None

per-profile Z heights, required when the profiles are 2-D

Return type:

Solid

Examples

Skinning a round profile up to a square one (a lofted transition):

import math
import numpy as np
from pybosl2 import VNF, Path2D
from pybosl2.enums import SkinMethod

circle = Path2D(
    [[6 * math.cos(t), 6 * math.sin(t)] for t in np.linspace(0, 2 * math.pi, 24, endpoint=False)]
)
square = Path2D([[-8, -8], [8, -8], [8, 8], [-8, 8]])
VNF.from_skin([circle, square], slices=20, method=SkinMethod.REINDEX, z=[0, 25]).show()
Loading 3-D preview…

⬇ Download STL mesh

pybosl2.vnf.VnfStyle

alias of VNFStyle

pybosl2.vnf.contour(f, isovalue, bounding_box, pixel_size=None, pixel_count=None, closed=True, exact_bounds=False)[source]

Generate 2-D contour paths at a given isovalue from a scalar field.

Uses marching squares on a uniform 2-D grid to trace the contour where f(x, y) == isovalue. Returns a list of closed (or open) polyline paths, each being a list of [x, y] points.

Parameters:
f : np.ndarray | Callable[[np.ndarray], np.ndarray]

A 2-D numpy array or a callable (N,2)→(N,) or (x,y)→float.

isovalue : float

Scalar threshold.

bounding_box : Bounds2D

A Bounds2D.

pixel_size : float | None

Isotropic pixel size.

pixel_count : int | None

Approximate total pixel count (ignored if pixel_size given).

closed : bool

If True, return only closed contour loops.

exact_bounds : bool

If True, use bounding_box exactly.

Returns:

A list of contour paths, each a list of [x, y] points.

Return type:

list[list[list[float]]]

Examples

import numpy as np
from pybosl2 import contour, Bounds2D
from pybosl2.path2d import Path2D

def field(p: np.ndarray) -> np.ndarray:
    return np.asarray(np.hypot(p[:, 0], p[:, 1]))
paths = contour(field, 10, Bounds2D(-15, -15, 15, 15, 30, 30), pixel_size=0.5)
Path2D(paths[0]).stroke(width=0.5).linear_extrude(height=2).show()
Loading 3-D preview…

⬇ Download STL mesh