Skip to content

Grid Operations

Handles the geometry pipeline from raw meshes to final output:

  1. Voxelization -- converts the max-volume mesh into a dense 3D boolean grid at the configured voxel_size.
  2. Surface sampling -- discretizes test-surface faces into evenly spaced points with outward-facing normals, respecting grid_step and coplanarity tolerance.
  3. Pruning -- removes small disconnected voxel clusters below min_voxels using connected-component labelling.
  4. Mesh reconstruction -- converts the carved voxel mask back into a triangle mesh, either as cubic voxel faces or via SDF smoothing + marching cubes + Taubin polishing.

urbansolarcarver.grid

UrbanSolarCarver — Grid utilities

Tools for sampling nearly planar geometry, converting meshes to voxels, cleaning occupancy, and building meshes. Used by the carving pipeline to generate point/normal sets, binary grids, and final surfaces.

Overview

Two meshing modes are supported:

1) Cubic (apply_smoothing == False) - Clean the binary grid with component pruning, a 3×3×3 majority filter, and one 6-connected closing. - Triangulate with a cubic voxel mesher (Kaolin dual-cubes).

2) Smoothed SDF (apply_smoothing == True) - Convert occupancy to a signed distance field (SDF) in voxel units. - Light Gaussian blur, then edge-preserving Perona–Malik diffusion applied only near the zero level. - Pick an iso value that preserves the original inside volume. - Extract a surface with marching cubes (trimesh + scikit-image). - Optionally run a tiny Taubin polish (0..6 iters) after cleanup.

Public API

sample_planar_surface(mesh, sample_step, include_boundary=True) Uniformly sample a strictly planar patch and return per-point normals.

discretize_surface_with_normals(mesh, sample_step, coplanarity_tol_deg=5.0) Collect point/normal pairs and an analysis mesh from near-planar components.

voxelize_mesh(mesh, voxel_size, margin_frac, device=None) Rasterize a watertight mesh into a padded cubic occupancy grid.

prune_voxels(voxels, min_voxels) Remove 26-connected components smaller than min_voxels.

prune_voxels_morph(voxels, min_voxels) Gentle cleanup for the cubic path: component prune, majority filter, single closing. Preserves thin slabs better than erosion/opening.

voxelize_and_clean(mesh, voxel_size, margin_frac, min_voxels) Convenience wrapper: voxelize_mesh then prune_voxels.

mesh_from_voxels(voxels, min_corner, voxel_size) Build a blocky mesh from the binary grid. Returns an unprocessed Trimesh.

mesh_from_voxels_smoothed(voxels, min_corner, voxel_size) Build a smooth mesh by contouring a smoothed SDF with marching cubes. Uses a volume-matched iso to avoid systematic thinning.

mesh_from_voxels_select(voxels, min_corner, voxel_size, apply_smoothing) Dispatch to the cubic or SDF path based on the flag above.

cleanup_mesh(mesh, min_face_count=100) Fix winding/normals, weld vertices, drop tiny fragments.

polish_mesh_taubin(mesh, iters=0) Optional micro-polish after SDF+MC. Scale-normalized. Safe for 0..6 iters.

Internal helpers

plane_frame(normal) Orthonormal in-plane basis for planar sampling.

_voxel_presmooth(field_bool) SDF build + Gaussian + narrow-band Perona–Malik diffusion.

_pm_anisotropic_diffuse(sdf, iters, k, tau) Edge-preserving diffusion stepper used by _voxel_presmooth.

_volume_matched_threshold(sdf, target_inside_voxels) Iso selection that matches the original inside count.

_median_edge_length(mesh) Median of unique edge lengths; used to normalize the Taubin polish.

Data types and units
  • Points and normals: np.ndarray, shape (N, 3), world units.
  • Voxel grids: torch.Tensor, shape (D, D, D), uint8/bool, 1 = inside.
  • Meshes: trimesh.Trimesh in world coordinates.
  • voxel_size is in world units and is passed to marching cubes as pitch.
Notes
  • The cubic path aims for stable, interpretable “voxel look” with minimal flicker between runs. No erosion by default to protect thin volumes.
  • The smoothed path operates in SDF space, not on the triangle mesh. This removes terracing while keeping the overall form.
  • The Taubin step is deliberately tiny and optional. Heavy smoothing belongs in SDF space, not post-mesh.

AnalysisMesh(vertices, faces, face_normals=None)

Lightweight quad mesh container. Holds raw numpy arrays and builds a Ladybug Mesh3D or trimesh Trimesh on demand.

Attributes:

Name Type Description
vertices (V, 3) float64 ndarray — quad corner positions.
faces (N, 4) int32 ndarray — quad face vertex indices.
face_normals (N, 3) float32 ndarray or None — outward unit normals per face.

to_dict()

Serialise to a plain dict (vertices + faces as nested lists).

to_ladybug_mesh3d()

Build a ladybug_geometry.geometry3d.mesh.Mesh3D (slow, use lazily).

to_trimesh(face_colors=None)

Triangulate quads and return a trimesh.Trimesh.

voxelize(envelope_mesh, config, device=None)

Rasterize the envelope into a padded cubic voxel grid.

Parameters:

Name Type Description Default
envelope_mesh Trimesh
required
config user_config
required
device device or None
None

Returns:

Name Type Description
voxel_grid (Tensor(D, D, D), uint8)

1 indicates inside/filled.

grid_origin ndarray(3)

World-space min corner for index (0,0,0).

grid_extent float

Physical cube size (meters).

grid_resolution int

Number of voxels per side (D).

Raises:

Type Description
RuntimeError

If voxelization returns an empty grid.

sample_surface(insolation_mesh, config)

Sample quasi-planar patches to produce point/normal sets for carving.

Returns:

Name Type Description
sample_points ndarray(N, 3)
sample_normals ndarray(N, 3)
analysis_mesh Mesh3D or None

The analysis mesh whose face centroids are the sample points.

Raises:

Type Description
RuntimeError

If no points are sampled from the insolation surface.

finalize_mesh(carved_grid, grid_origin, config)

Convert carved voxels to a mesh and clean results.

Behavior

• If apply_smoothing is False: prune small components + gentle morphology (majority + closing), cubic meshing, then cleanup_mesh. • If apply_smoothing is True: prune small components, SDF presmooth + marching cubes, then cleanup_mesh, optional micro Taubin polish capped to 0..6 iterations.

Parameters:

Name Type Description Default
carved_grid Tensor(D, D, D)

Binary occupancy after carving.

required
grid_origin ndarray(3)

World-space min corner for index (0,0,0).

required
config user_config
required

Returns:

Name Type Description
cleaned_voxels Tensor(D, D, D)

Post-pruned (and possibly morphologically cleaned) occupancy.

initial_mesh Trimesh

Mesh directly from the selected meshing path (pre-cleanup copy).

final_mesh Trimesh

Cleaned (and optionally micro-polished) mesh.

plane_frame(surface_normal)

Return two orthonormal vectors spanning the plane orthogonal to surface_normal.

Parameters:

Name Type Description Default
surface_normal (3,) array_like

Plane normal (need not be unit length).

required

Returns:

Type Description
axis_u, axis_v : (3,) np.ndarray

Unit vectors forming a right-handed 2D basis in the plane.

Notes
  • The normal is normalized internally.
  • A stable reference axis is chosen to avoid degeneracy when the normal is almost aligned with X.
  • Used by sample_planar_surface to build a local 2D sampling frame.

sample_planar_surface(mesh, sample_step=1.0, include_boundary=True)

Uniformly sample points on a strictly planar submesh and return per-point normals.

Parameters:

Name Type Description Default
mesh Trimesh

Single planar connected component (convex or with holes).

required
sample_step float

Spacing of the sampling grid in world units.

1.0
include_boundary bool

If True, include points that fall on polygon boundaries; otherwise drop them.

True

Returns:

Name Type Description
points (N, 3) float32 np.ndarray

Sample locations in world coordinates.

normals (N, 3) float32 np.ndarray

Unit normals (constant over the patch, oriented consistently with the mesh).

Notes
  • Builds a local 2D grid in the plane (see plane_frame) and rasterizes the polygon footprint to select in-polygon grid sites.
  • Used by discretize_surface_with_normals for planar-only sampling.

discretize_surface_with_normals(mesh, sample_step=1.0, coplanarity_tol_deg=5.0)

Sample nearly planar regions of a mesh and return point/normal pairs.

Uses the fast Shapely-based rasterizer (sample_planar_surface) for point/normal generation and builds a quad analysis mesh from the same 2D grid. The mesh has a 1:1 face-to-point mapping suitable for per-face coloring (e.g. "LB Spatial Heatmap" in GH/Rhino).

Parameters:

Name Type Description Default
mesh Trimesh

Input triangle mesh (test surface).

required
sample_step float

Sampling grid spacing in world units.

1.0
coplanarity_tol_deg float

Maximum face-normal deviation (degrees) to consider a component planar.

5.0

Returns:

Name Type Description
points (N, 3) float32 np.ndarray

Sample points (centroids of the analysis-mesh quads).

normals (N, 3) float32 np.ndarray

Outward-facing normals at each sample point.

analysis_mesh AnalysisMesh or None

Combined quad analysis mesh, or None if no points were sampled.

voxelize_and_clean(mesh, voxel_size=1.0, margin_frac=0.2, min_voxels=100)

Convenience wrapper: voxelize a mesh and prune tiny connected components.

Returns:

Name Type Description
voxels (D, D, D) uint8 torch.Tensor

Binary occupancy (1 = inside/filled).

origin (3,) np.ndarray

World-space min corner of the grid.

grid_extent float

Physical cube size covered by the grid.

resolution int

Voxel resolution (D).

See Also

voxelize_mesh, prune_voxels

voxelize_mesh(mesh, voxel_size=1.0, margin_frac=0.2, device=None)

Rasterize a watertight mesh into a padded binary voxel grid.

Parameters:

Name Type Description Default
mesh Trimesh
required
voxel_size float

Edge length of one voxel in world units.

1.0
margin_frac float

Padding on each side as a fraction of the mesh AABB's max side.

0.2
device device or None

Target device for the returned tensor.

None

Returns:

Name Type Description
voxels (D, D, D) uint8 torch.Tensor

1 where inside/filled, 0 otherwise.

origin (3,) np.ndarray

World-space min corner of the grid.

grid_extent float

Physical size of the cubic domain.

resolution int

Number of voxels per side (D).

Notes
  • Uses trimesh ray-based voxelization and scipy cavity fill.
  • Grid is axis-aligned and cubic by construction.
  • No GPU dependency for voxelization itself (runs on CPU via trimesh).

prune_voxels(voxels, min_voxels)

Remove connected components smaller than min_voxels (26-connectivity).

Parameters:

Name Type Description Default
voxels (D, D, D) uint8/bool torch.Tensor
required
min_voxels int

Size threshold; components with fewer voxels are removed.

required

Returns:

Name Type Description
cleaned (D, D, D) uint8 torch.Tensor

Binary occupancy on the same device as voxels.

Notes
  • Used on the smoothed path (apply_smoothing=True) prior to SDF smoothing.

prune_voxels_morph(voxels, min_voxels)

Gentle cleanup for the cubic (unsmoothed) path.

Steps
  1. Remove tiny connected components (26-connectivity).
  2. Apply a 3×3×3 majority filter (keeps voxels with ≥3 neighbors) to suppress isolated specks.
  3. Run a single binary closing with a 6-connected structuring element to seal pinholes without shrinking slabs.

Parameters:

Name Type Description Default
voxels (D, D, D) uint8/bool torch.Tensor
required
min_voxels int
required

Returns:

Name Type Description
cleaned (D, D, D) uint8 torch.Tensor

polish_mesh_taubin(mesh, iters=2)

Apply a tiny, scale-normalized Taubin pass to knock down micro-ripples.

Parameters:

Name Type Description Default
mesh Trimesh
required
iters int

Small integer in [0, 6]. If 0, the mesh is returned unchanged.

2

Returns:

Name Type Description
mesh Trimesh
Notes
  • Uses fixed stable parameters (λ≈0.28, ν≈−0.31) and normalizes by the median edge length to avoid scale sensitivity.
  • Intended only after the SDF+MC path. Keep very small to avoid shape drift.

mesh_from_voxels(voxels, min_corner, voxel_size)

Construct a blocky (cubic) mesh from a binary occupancy grid.

Parameters:

Name Type Description Default
voxels (D, D, D) uint8/bool torch.Tensor
required
min_corner (3,) array_like

World-space origin of the grid (maps index [0,0,0] to this point).

required
voxel_size float

Size of one voxel in world units.

required

Returns:

Name Type Description
mesh Trimesh

Unprocessed triangle mesh (process=False). Call cleanup_mesh next.

See Also

mesh_from_voxels_smoothed, mesh_from_voxels_select

mesh_from_voxels_smoothed(voxels, min_corner, voxel_size)

Construct a smooth mesh by conturing a smoothed SDF with marching cubes.

Parameters:

Name Type Description Default
voxels (D, D, D) uint8/bool torch.Tensor

Binary occupancy (1 = inside).

required
min_corner (3,) array_like

World-space grid origin.

required
voxel_size float

World units per voxel (passed as pitch to marching cubes).

required

Returns:

Name Type Description
mesh Trimesh

Triangle mesh positioned in world coordinates.

Notes
  • Internally calls _voxel_presmooth and chooses an iso-value via _volume_matched_threshold to preserve volume.

mesh_from_voxels_select(voxels, min_corner, voxel_size, apply_smoothing)

Dispatch to cubic or SDF-smoothed meshing based on apply_smoothing.

Returns:

Name Type Description
mesh Trimesh
See Also

mesh_from_voxels, mesh_from_voxels_smoothed

cleanup_mesh(mesh, min_face_count=100, light=False)

Repair and prune small fragments from a mesh.

Parameters:

Name Type Description Default
mesh Trimesh
required
min_face_count int

Fragments with fewer faces than this are discarded.

100
light bool

If True, skip expensive repair steps (fill_holes, fix_inversion, merge_vertices). Use for cubic meshes which are already watertight with correct winding. ~10x faster on large meshes.

False

Returns:

Name Type Description
mesh Trimesh

Cleaned mesh (may be a concatenation of surviving components).