API Reference
Core Types
The fundamental data structures for representing point clouds.
WhatsThePoint.AbstractSurface — Type
abstract type AbstractSurface{M<:Manifold,C<:CRS} endA surface of a PointSurface.
WhatsThePoint.PointSurface — Type
struct PointSurface{M,C,S,T} <: AbstractSurface{M,C}This is a typical representation of a surface via points.
Type Parameters
M<:Manifold- manifold typeC<:CRS- coordinate reference systemS- shadow typeT<:AbstractTopology- topology type for surface-local connectivity
WhatsThePoint.SurfaceElement — Type
struct SurfaceElement{M,C,N,A}Representation of a point on a <:PointSurface.
WhatsThePoint.PointBoundary — Type
struct PointBoundary{M,C} <: Domain{M,C}A boundary of points.
Fields
surfaces: Named surfaces forming the boundary
Type Parameters
M <: Manifold: The manifold typeC <: CRS: The coordinate reference system
WhatsThePoint.PointVolume — Type
struct PointVolume{M,C,T,V} <: Domain{M,C}Interior volume points with optional topology.
Type Parameters
M<:Manifold- manifold typeC<:CRS- coordinate reference systemT<:AbstractTopology- topology type for volume-local connectivityV<:AbstractVector{Point{M,C}}- storage type (allows GPU arrays)
WhatsThePoint.PointCloud — Type
struct PointCloud{M,C,T} <: Domain{M,C}A point cloud with optional topology (connectivity).
Type Parameters
M<:Manifold- manifold typeC<:CRS- coordinate reference systemT<:AbstractTopology- topology type for cloud-level connectivity
Accessors
Common accessor functions for point cloud types.
WhatsThePoint.points — Function
points(surf::PointSurface)Return vector of points from surface. Alias for point(surf).
points(vol::PointVolume)Return vector of points from volume.
points(boundary::PointBoundary)Return vector of all points from all surfaces in the boundary.
points(cloud::PointCloud)Return vector of all points (boundary + volume).
WhatsThePoint.point — Function
point(surf::PointSurface)Return the vector of point coordinates for all surface elements.
Meshes.normal — Function
normal(surf::PointSurface)Return the vector of outward unit normal vectors for all surface elements.
Meshes.area — Function
area(surf::PointSurface)Return the vector of surface areas for all surface elements.
WhatsThePoint.topology — Function
topology(surf::PointSurface)Return the topology of the surface.
topology(vol::PointVolume)Return the topology of the volume.
topology(cloud::PointCloud)Return the topology of the point cloud.
Topology
Point connectivity for meshless stencils.
WhatsThePoint.AbstractTopology — Type
abstract type AbstractTopology{S}Abstract base type for point cloud topology (connectivity). Type parameter S is the storage format for neighbor indices.
WhatsThePoint.NoTopology — Type
struct NoTopology <: AbstractTopology{Nothing}Singleton type representing no topology. Default for PointCloud.
WhatsThePoint.KNNTopology — Type
mutable struct KNNTopology{S} <: AbstractTopology{S}k-nearest neighbors topology.
Fields
neighbors::S- neighbor indices storagek::Int- number of neighbors per point
WhatsThePoint.RadiusTopology — Type
mutable struct RadiusTopology{S,R} <: AbstractTopology{S}Radius-based topology where neighbors are all points within a given radius.
Fields
neighbors::S- neighbor indices storageradius::R- search radius (scalar or function of position)
WhatsThePoint.set_topology — Function
set_topology(surf::PointSurface, ::Type{KNNTopology}, k::Int)Build and return new surface with k-nearest neighbor topology.
set_topology(surf::PointSurface, ::Type{RadiusTopology}, radius)Build and return new surface with radius-based topology.
set_topology(vol::PointVolume, ::Type{KNNTopology}, k::Int)Build and return new volume with k-nearest neighbor topology.
set_topology(vol::PointVolume, ::Type{RadiusTopology}, radius)Build and return new volume with radius-based topology.
set_topology(cloud::PointCloud, ::Type{KNNTopology}, k::Int)Build and return new cloud with k-nearest neighbor topology.
set_topology(cloud::PointCloud, ::Type{RadiusTopology}, radius)Build and return new cloud with radius-based topology.
WhatsThePoint.rebuild_topology! — Function
rebuild_topology!(topo::NoTopology, points)No-op for NoTopology (nothing to rebuild).
rebuild_topology!(topo::KNNTopology, points)Rebuild k-nearest neighbor topology in place.
rebuild_topology!(topo::RadiusTopology, points)Rebuild radius-based topology in place.
rebuild_topology!(surf::PointSurface)Rebuild topology in place using same parameters. No-op if NoTopology.
rebuild_topology!(vol::PointVolume)Rebuild topology in place using same parameters. No-op if NoTopology.
rebuild_topology!(cloud::PointCloud)Rebuild topology in place using same parameters. No-op if NoTopology.
WhatsThePoint.neighbors — Function
neighbors(t::AbstractTopology)Return the neighbor storage from a topology.
neighbors(t::AbstractTopology, i::Int)Return neighbors of point i.
neighbors(surf::PointSurface)Return all neighbor lists from the surface topology. Throws error if no topology.
neighbors(surf::PointSurface, i::Int)Return neighbors of point i in surface-local indices. Throws error if no topology.
neighbors(vol::PointVolume)Return all neighbor lists from the volume topology. Throws error if no topology.
neighbors(vol::PointVolume, i::Int)Return neighbors of point i in volume-local indices. Throws error if no topology.
neighbors(cloud::PointCloud)Return all neighbor lists from the topology. Throws error if no topology or invalid.
neighbors(cloud::PointCloud, i::Int)Return neighbors of point i. Throws error if no topology or invalid.
WhatsThePoint.hastopology — Function
hastopology(surf::PointSurface)Check if surface has a topology (not NoTopology).
hastopology(vol::PointVolume)Check if volume has a topology (not NoTopology).
hastopology(cloud::PointCloud)Check if point cloud has a topology (not NoTopology).
Discretization
Volume point generation algorithms and spacing types.
Meshes.discretize — Function
discretize(bnd::PointBoundary, spacing; alg=auto, max_points=10_000_000)Generate volume points for the given boundary and return a new PointCloud.
spacing can be either an AbstractSpacing object or a bare Unitful.Length value (which will be wrapped in ConstantSpacing).
Keyword Arguments
alg: Discretization algorithm (default:SlakKosec()for 3D)max_points: Maximum number of volume points to generate
Example
mesh = GeoIO.load("model.stl").geometry
boundary = PointBoundary(mesh)
octree = TriangleOctree(mesh; h_min=0.1)
cloud = discretize(boundary, 3.0m; alg=SlakKosec(octree), max_points=100_000)discretize(cloud::PointCloud, spacing; alg=auto, max_points=10_000_000)Generate volume points for an existing cloud and return a new PointCloud with the volume populated.
discretize(bnd::PointBoundary{𝔼{3}}, alg::OctreeRandom; max_points=10_000_000)Generate volume points using OctreeRandom without requiring a spacing parameter.
OctreeRandom generates uniformly random points and does not use spacing, so this overload removes the need for a dummy spacing value.
Example
mesh = GeoIO.load("bunny.stl").geometry
boundary = PointBoundary(mesh)
cloud = discretize(boundary, OctreeRandom(mesh); max_points=100_000)WhatsThePoint.SlakKosec — Type
SlakKosec <: AbstractNodeGenerationAlgorithmSlak-Kosec algorithm for volume point generation with optional octree acceleration.
The algorithm generates candidate points on spheres around existing points and accepts them if they are inside the domain and sufficiently far from existing points.
Fields
n::Int- Number of candidate points per sphere (default: 10)octree::Union{Nothing,TriangleOctree}- Optional octree for fast isinside queries
Constructors
SlakKosec() # Default: n=10, no octree
SlakKosec(20) # Custom n, no octree
SlakKosec(octree::TriangleOctree) # Use octree acceleration with n=10
SlakKosec(20, octree) # Custom n with octree accelerationPerformance
- Without octree: Uses Green's function for isinside (~50ms per query)
- With octree: Uses spatial indexing (~0.05ms per query, 1000× faster!)
Usage Examples
Standard Usage (Green's function)
using WhatsThePoint
# Load boundary
boundary = PointBoundary("model.stl")
cloud = PointCloud(boundary)
# Discretize without octree (slow for large domains)
spacing = ConstantSpacing(1.0u"m")
result = discretize(cloud, spacing; alg=SlakKosec(), max_points=10_000)Octree-Accelerated Usage (Recommended for large domains)
using WhatsThePoint
# Load boundary points
boundary = PointBoundary("model.stl")
cloud = PointCloud(boundary)
# Build octree from STL file (Option 1: simplest)
octree = TriangleOctree("model.stl"; h_min=0.01, classify_leaves=true)
# Or from SimpleMesh (Option 2)
# mesh = GeoIO.load("model.stl").geometry
# octree = TriangleOctree(mesh; h_min=0.01, classify_leaves=true)
# Use octree-accelerated discretization (100-1000× faster!)
spacing = ConstantSpacing(1.0u"m")
alg = SlakKosec(octree) # Pass octree to algorithm
result = discretize(cloud, spacing; alg=alg, max_points=100_000)References
Šlak J, Kosec G. "On generation of node distributions for meshless PDE discretizations" (2019)
WhatsThePoint.VanDerSandeFornberg — Type
VanDerSandeFornberg <: AbstractNodeGenerationAlgorithm3D volume discretization algorithm that projects a 2D grid onto the shadow plane and fills the volume layer by layer using sphere packing heights. Requires ConstantSpacing.
See: Van der Sande, K. & Fornberg, B. (2021). SIAM J. Sci. Comput., 43(1).
WhatsThePoint.FornbergFlyer — Type
FornbergFlyer <: AbstractNodeGenerationAlgorithm2D volume discretization algorithm using a height-field approach projected onto the x-axis. This is the default and only algorithm for 2D boundaries. Requires ConstantSpacing.
See: Fornberg, B. & Flyer, N. (2015). Comput. Math. Appl., 69(7).
WhatsThePoint.OctreeRandom — Type
OctreeRandom <: AbstractNodeGenerationAlgorithmOctree-guided random point generation for volume discretization.
This algorithm uses a TriangleOctree with leaf classification to efficiently generate random points inside the mesh. It's much faster than rejection sampling because:
- Only samples in interior/boundary regions (skips exterior entirely)
- Interior boxes need no filtering (100% acceptance rate)
- Only boundary boxes require isinside() checks
Fields
octree::TriangleOctree- Octree with leaf classificationboundary_oversampling::Float64- Oversampling factor for boundary boxes (default: 2.0)verify_interior::Bool- Per-point signed distance verification for interior leaves (default: false)
Constructors
# From mesh (recommended) — builds octree automatically with classified leaves
OctreeRandom(mesh::SimpleMesh) # Auto h_min
OctreeRandom(mesh; h_min=0.01, boundary_oversampling=2.0) # Custom h_min
# From file path — loads mesh then builds octree
OctreeRandom("bunny.stl")
OctreeRandom("bunny.stl"; h_min=0.01)
# From pre-built octree (advanced)
OctreeRandom(octree::TriangleOctree)
OctreeRandom(octree, oversampling; verify_interior=false)Performance
- Interior leaves: O(1) per point (no filtering needed)
- Boundary leaves: O(k) per point where k ≈ 10-50 triangles
- Much faster than bounding box rejection: Typical acceptance 5-20% vs 90%+ here
Usage Examples
Recommended Usage
using WhatsThePoint
mesh = GeoIO.load("bunny.stl").geometry
boundary = PointBoundary(mesh)
cloud = discretize(boundary, OctreeRandom(mesh); max_points=100_000)With Spacing (backward compatible)
octree = TriangleOctree(mesh; h_min=0.01, classify_leaves=true)
cloud = discretize(boundary, 1.0u"m"; alg=OctreeRandom(octree), max_points=10_000)With Custom Oversampling
alg = OctreeRandom(mesh; boundary_oversampling=2.5)
cloud = discretize(boundary, alg; max_points=10_000)Notes
- The mesh convenience constructor always sets
classify_leaves=true - When
h_minis omitted, it is computed asbbox_diagonal / (2 * cbrt(n_triangles)) - The
spacingparameter is not used (random uniform distribution) - For spacing-based discretization, use SlakKosec or VanDerSandeFornberg
- Actual point count may be slightly less than
max_pointsdue to boundary filtering - Oversampling 1.5-3.0 is recommended; higher values waste computation
Algorithm Details
- Identify regions: Separate octree leaves into interior (2) and boundary (1)
- Allocate points: Distribute target count proportionally to leaf volumes
- Generate interior points: Random sampling in interior boxes (no filtering)
- Generate boundary points: Oversample and filter with isinside()
- Return: Combined set of validated interior points
When to Use
Good for:
- Quick initial discretization
- Uniform random distributions
- Maximum point count targets
- Testing and prototyping
Not ideal for:
- Spacing-controlled discretization (use SlakKosec instead)
- Adaptive refinement
- Smooth point distributions
WhatsThePoint.ConstantSpacing — Type
ConstantSpacing{L<:Unitful.Length} <: AbstractSpacingConstant node spacing.
WhatsThePoint.LogLike — Type
LogLike <: VariableSpacingNode spacing based on a log-like function of the distance to nearest boundary $x/(x+a)$ where $x$ is the distance to the nearest boundary and $a$ is a parameter to control the growth rate as $a = 1 - (g - 1)$ where $g$ is the conventional growth rate parameter.
WhatsThePoint.Power — Type
Power <: VariableSpacingNode spacing based on a power of the distance to nearest boundary $x^{g}$ where $x$ is the distance to the nearest boundary and $g$ is the growth_rate.
Boundary Operations
Normal computation and surface manipulation.
WhatsThePoint.compute_normals — Function
compute_normals(surf::PointSurface{𝔼{N},C}; k::Int=5) where {N,C<:CRS}Estimate the normals of a set of points that form a surface. Uses the PCA approach from "Surface Reconstruction from Unorganized Points" - Hoppe (1992).
Requires Euclidean manifold (𝔼{2} or 𝔼{3}). This function assumes flat space geometry.
compute_normals(search_method::KNearestSearch, surf::PointSurface{𝔼{N},C}) where {N,C<:CRS}Estimate the normals of a set of points that form a surface. Uses the PCA approach from "Surface Reconstruction from Unorganized Points" - Hoppe (1992).
Requires Euclidean manifold (𝔼{2} or 𝔼{3}). This function assumes flat space geometry.
WhatsThePoint.orient_normals! — Function
orient_normals!(search_method::KNearestSearch, normals::AbstractVector{<:AbstractVector}, points)Correct the orientation of normals on a surface as the compute_normals function does not guarantee if the normal is inward or outward facing. Uses the approach from "Surface Reconstruction from Unorganized Points" - Hoppe (1992).
orient_normals!(normals::AbstractVector{<:AbstractVector}, points::AbstractVector{<:Point{𝔼{N}}}; k::Int=5) where {N}Correct the orientation of normals on a surface as the compute_normals function does not guarantee if the normal is inward or outward facing. Uses the approach from "Surface Reconstruction from Unorganized Points" - Hoppe (1992).
Requires Euclidean manifold (𝔼{2} or 𝔼{3}). This function uses Euclidean dot products for orientation consistency.
WhatsThePoint.update_normals! — Function
update_normals!(surf::PointSurface{𝔼{N},C}; k::Int=5) where {N,C<:CRS}Update the normals of the boundary of a surf. This is necessary whenever the points change for any reason.
Requires Euclidean manifold (𝔼{2} or 𝔼{3}). This function assumes flat space geometry.
WhatsThePoint.split_surface! — Function
split_surface!(cloud, angle; k=10)
split_surface!(cloud, target_surf, angle; k=10)Split a surface into sub-surfaces based on normal angle discontinuities. Builds a k-nearest neighbor graph, removes edges where adjacent normals differ by more than angle, and labels each connected component as a separate named surface.
When called on a cloud/boundary with a single surface, that surface is split automatically. When multiple surfaces exist, specify target_surf by name.
WhatsThePoint.combine_surfaces! — Function
combine_surfaces!(boundary::PointBoundary, surfs...)Merge multiple named surfaces into one. The first name is kept and subsequent surfaces are merged into it. All original surfaces are removed and replaced by the combined surface.
Shadow Points
Virtual points offset inward from the boundary for Hermite-type boundary condition enforcement.
WhatsThePoint.ShadowPoints — Type
ShadowPoints(Δ, order=1)
ShadowPoints(Δ::Number, order)Shadow point configuration for generating virtual points offset inward from the boundary. Δ is the offset distance (constant or a function of position). order is the derivative order for Hermite-type boundary condition enforcement.
WhatsThePoint.generate_shadows — Function
generate_shadows(points, normals, shadow::ShadowPoints)
generate_shadows(surf::PointSurface, shadow::ShadowPoints)
generate_shadows(cloud::PointCloud, shadow::ShadowPoints)Generate shadow points offset inward from the boundary along the normal direction by the distance specified in shadow. Returns a vector of Point objects.
Geometry and Queries
Point-in-volume testing, octree acceleration, and spatial utilities.
WhatsThePoint.isinside — Function
isinside(point::SVector{3,T}, octree::TriangleOctree) -> BoolFast interior/exterior test using octree spatial index.
Returns true if point is inside the closed surface defined by the mesh.
Performance
- Complexity: O(log M + k) where M = number of triangles, k ≈ 10-50
- Speedup: 100-1000× faster than brute-force O(M) approach
Example
using WhatsThePoint, StaticArrays
octree = TriangleOctree("model.stl"; h_min=0.01, classify_leaves=true)
point = SVector(0.5, 0.5, 0.5)
is_inside = isinside(point, octree)isinside(points::Vector{SVector{3,T}}, octree::TriangleOctree) -> Vector{Bool}Batch interior test for multiple points.
Example
test_points = [SVector(randn(3)...) for _ in 1:1000]
results = isinside(test_points, octree) # Returns Vector{Bool}isinside(point::Point{𝔼{3}}, octree::TriangleOctree{𝔼{3},C,T}) -> BoolBridge from Meshes.jl Point to SVector-based isinside.
Extracts coordinates from Point, converts to SVector{3,T}, and delegates to the SVector method.
isinside(points::AbstractVector{<:Point{𝔼{3}}}, octree::TriangleOctree{𝔼{3}}) -> Vector{Bool}Batch bridge from Meshes.jl Points to SVector-based isinside.
WhatsThePoint.TriangleOctree — Type
TriangleOctree{M<:Manifold, C<:CRS, T<:Real}An octree-based spatial index for efficient triangle mesh queries.
Fields
tree::SpatialOctree{Int,T}: Underlying octree storing triangle indicesmesh::SimpleMesh{M,C}: Original Meshes.jl mesh (single source of truth for triangle data)leaf_classification::Union{Nothing, Vector{Int8}}: Classification of empty leaves0: Exterior (outside mesh)1: Boundary (near surface but empty)2: Interior (inside mesh)
Performance
For a mesh with M triangles:
- Construction: O(M log M) - distribute triangles to boxes
- Query: O(log M + k) where k ≈ 10-50 triangles per leaf
- Memory: O(N) for octree nodes (mesh data accessed on-the-fly)
Example
# Option 1: From STL file (simplest)
octree = TriangleOctree("model.stl"; h_min=0.01, classify_leaves=true)
# Option 2: From SimpleMesh
mesh = GeoIO.load("model.stl").geometry
octree = TriangleOctree(mesh; h_min=0.01, classify_leaves=true)
# Fast queries (100-1000x speedup over brute force)
point = SVector(0.5, 0.5, 0.5)
is_inside = isinside(point, octree)Workflow with PointBoundary
Since PointBoundary discards mesh topology, build both from the same mesh:
mesh = GeoIO.load("model.stl").geometry
boundary = PointBoundary(mesh)
octree = TriangleOctree(mesh; h_min=0.01, classify_leaves=true)
cloud = discretize(boundary, spacing; alg=OctreeRandom(octree))WhatsThePoint.has_consistent_normals — Function
has_consistent_normals(mesh::SimpleMesh) -> BoolCheck if triangle faces are consistently oriented (manifold orientation test).
This function verifies that all shared edges between adjacent triangles are traversed in OPPOSITE directions, which is the geometric requirement for a properly oriented manifold surface. This test is independent of surface curvature.
Algorithm
For each shared edge between two triangles:
- Triangle A has edge v1→v2
- Triangle B (adjacent) must have edge v2→v1 (opposite direction)
- If both traverse in the same direction → faces are incorrectly oriented
Returns
trueif all triangles are correctly oriented (manifold surface)falseif any triangles have flipped faces (orientation errors)
Performance
O(n) complexity using edge hash map instead of O(n²) pairwise comparison.
Examples
octree = TriangleOctree("model.stl"; h_min=0.01)
if !has_consistent_normals(octree.mesh)
@warn "Mesh has flipped triangles - will cause incorrect isinside() results"
endNote
This test uses GEOMETRIC edge orientation, not algebraic normal dot products. It will correctly validate meshes with high curvature (sharp edges, creases).
WhatsThePoint.emptyspace — Function
emptyspace(testpoint, points)Check if a point occupies empty space within a certain tolerance.
Meshes.boundingbox — Function
boundingbox(pts::AbstractVector{<:Point})Compute the axis-aligned bounding box of a collection of points.
Meshes.centroid — Function
centroid(pts::AbstractVector{<:Point})Compute the centroid (geometric center) of a collection of points.
Node Repulsion
Point distribution optimization.
WhatsThePoint.repel — Function
repel(cloud::PointCloud, spacing; β=0.2, α=auto, k=21, max_iters=1000, tol=1e-6)Optimize point distribution via node repulsion (Miotti 2023). Returns (new_cloud, convergence_vector) tuple.
The returned cloud has NoTopology since points have moved.
Diagnostics
WhatsThePoint.metrics — Function
metrics(cloud::PointCloud; k=20)Print distance statistics (mean, std, max, min) to the k nearest neighbors for all points in the cloud. Useful for assessing point distribution quality before and after repulsion.
I/O
WhatsThePoint.import_surface — Function
import_surface(filepath::String)Load a surface mesh from a file (STL, OBJ, or any format supported by GeoIO.jl). Returns a tuple of (points, normals, areas, mesh) where points are face centers.
WhatsThePoint.export_cloud — Function
export_cloud(filename::String, cloud::PointCloud)Export a point cloud to VTK format. The output file contains boundary point coordinates and normal vectors.
FileIO.save — Function
save(filename::String, cloud::PointCloud)Save a point cloud to a file using FileIO.jl serialization.
Unexported API
WhatsThePoint.AbstractOctree — Type
AbstractOctree{E,T} = AbstractSpatialTree{3,E,T}Convenience alias for 3D spatial trees (octrees).
WhatsThePoint.AbstractSpatialTree — Type
AbstractSpatialTree{N,E,T}Abstract type for N-dimensional spatial trees.
Type Parameters
N::Int: Spatial dimensionality (2 for quadtree, 3 for octree)E: Element type stored in tree (e.g., Int for indices)T<:Real: Coordinate numeric type (Float64, Float32, etc.)
Interface Requirements
Trees must implement:
find_leaf(tree, point)- Locate leaf containing pointbounding_box(tree)- Get overall boundsnum_elements(tree)- Total elements stored
Optional:
find_neighbors(tree, box_idx, direction)- Neighbor queriesbalance!(tree)- Enforce refinement constraints
WhatsThePoint.AndCriterion — Type
AndCriterion{T<:Tuple} <: SubdivisionCriterionCombine multiple criteria - all must be satisfied for subdivision.
Fields
criteria::T: Tuple of criteria to combine (parametrized for type stability)
WhatsThePoint.MaxElementsCriterion — Type
MaxElementsCriterion <: SubdivisionCriterionSubdivide box if number of elements exceeds threshold.
Fields
max_elements::Int: Maximum elements before subdivision
WhatsThePoint.NearestTriangleState — Type
NearestTriangleState{T<:Real}Mutable state for nearest-triangle search, replacing 3 separate Ref allocations with a single struct (1 heap allocation instead of 3, enables potential SROA).
WhatsThePoint.SizeCriterion — Type
SizeCriterion{T<:Real} <: SubdivisionCriterionSubdivide box if size exceeds threshold.
Fields
h_min::T: Minimum box size (stop subdividing when reached)
WhatsThePoint.SpatialOctree — Type
SpatialOctree{E,T<:Real} <: AbstractOctree{E,T}Concrete octree implementation using integer coordinate system for efficient neighbor finding.
Uses (i,j,k,N) coordinate system where:
- (i,j,k) are integer coordinates at refinement level N
- Box center = origin + (2*[i,j,k] + 1) * (root_size / N) / 2
- Enables O(1) neighbor calculation from coordinates
Type Parameters
E: Type of elements stored (e.g., Int for triangle IDs)T: Coordinate numeric type (e.g., Float64, Float32)
Fields
parent::Vector{Int}: Parent box index for each node (0 = root has no parent)children::Vector{SVector{8,Int}}: 8 child indices per box (0 = no child)coords::Vector{SVector{4,Int}}: (i,j,k,N) coordinates per box where N is refinement levelorigin::SVector{3,T}: Spatial origin of root boxroot_size::T: Size of root box (assumed cubic)element_lists::Vector{Vector{E}}: Elements in each boxnum_boxes::Ref{Int}: Current number of boxes (mutable counter)
Interface Implementation
Implements AbstractSpatialTree interface:
find_leaf(tree, point)- O(log n) point locationbounding_box(tree)- Root box boundsnum_elements(tree)- Total stored elementsfind_neighbor(tree, box, dir)- 6-directional neighbor findingbalance_octree!(tree)- 2:1 refinement constraint
Example
using StaticArrays
origin = SVector(0.0, 0.0, 0.0)
octree = SpatialOctree{Int,Float64}(origin, 10.0)
# Subdivide root
subdivide!(octree, 1)
# Find leaf containing point
point = SVector(2.0, 2.0, 2.0)
leaf = find_leaf(octree, point)WhatsThePoint.SpatialOctree — Method
SpatialOctree{E,T}(origin::SVector{3,T}, size::T; initial_capacity=1000)Create empty octree with root node.
Arguments
origin: Minimum corner of root boxsize: Edge length of root box (assumed cubic)initial_capacity: Initial array capacity (will grow as needed)
Example
origin = SVector(0.0, 0.0, 0.0)
octree = SpatialOctree{Int,Float64}(origin, 10.0)WhatsThePoint.SubdivisionCriterion — Type
SubdivisionCriterionAbstract type for subdivision decision logic.
Allows pluggable subdivision criteria via dispatch.
Built-in Criteria
MaxElementsCriterion(max_elements)- Subdivide if too many elementsSizeCriterion(h_min)- Subdivide until box small enoughAndCriterion(criteria...)- All criteria must be satisfied
Example
criterion = AndCriterion((
MaxElementsCriterion(50),
SizeCriterion(0.1)
))Base.filter — Method
filter(f::Function, vol::PointVolume)Return new PointVolume with only points satisfying predicate f. Topology is stripped since point indices change.
Base.isvalid — Method
isvalid(t::AbstractTopology)Check if topology is valid. With immutable design, topology is always valid if it exists.
WhatsThePoint._allocate_counts_by_volume — Method
_allocate_counts_by_volume(volumes, total_count; ensure_one=false)Allocate integer counts across leaves proportionally to volumes using largest-remainder rounding, preserving exact total count.
If ensure_one=true and total_count >= length(volumes), each leaf gets at least one allocation before distributing the remainder by volume.
WhatsThePoint._auto_h_min — Method
_auto_h_min(::Type{T}, mesh::SimpleMesh) where {T}Compute a default h_min for octree construction based on mesh geometry.
Heuristic: bbox_diagonal / (2 * cbrt(n_triangles)). Scales leaf size to the mesh's characteristic triangle spacing so the octree resolves surface detail without excessive subdivision.
WhatsThePoint._build_knn_neighbors — Method
_build_knn_neighbors(points, k::Int) -> Vector{Vector{Int}}Build k-nearest neighbor adjacency list from points.
WhatsThePoint._build_radius_neighbors — Method
_build_radius_neighbors(points, radius) -> Vector{Vector{Int}}Build radius-based adjacency list from points.
WhatsThePoint._classify_empty_leaf_conservative — Method
_classify_empty_leaf_conservative(tree, mesh, leaf_idx) -> Int8Conservative classification for empty leaves.
2(interior): center and all 8 corners classify inside0(exterior): center and all 8 corners classify outside1(boundary): mixed results
This avoids falsely labeling a partially-outside leaf as interior.
Implementation uses octree-accelerated signed distance, not ray casting.
WhatsThePoint._classify_leaves — Method
_classify_leaves(tree::SpatialOctree{Int,T}, mesh::SimpleMesh) -> Vector{Int8}Classify octree leaves as exterior (0), boundary (1), or interior (2).
Uses octree-accelerated signed distance for robust classification.
Returns
classification::Vector{Int8}: Classification for each box-1: Internal node (not a leaf, unclassified)0: Exterior leaf1: Boundary leaf2: Interior leaf
WhatsThePoint._collect_classified_leaves — Method
_collect_classified_leaves(octree::TriangleOctree)Collect interior and boundary leaves with their volumes from a classified octree.
Returns (interior_leaves, interior_volumes, boundary_leaves, boundary_volumes).
WhatsThePoint._compute_bbox — Method
_compute_bbox(::Type{T}, mesh::SimpleMesh) where {T} -> (bbox_min, bbox_max)Compute bounding box from mesh triangle data.
Performance: Single-pass min/max accumulation over all triangles.
WhatsThePoint._compute_local_signed_distance — Method
_compute_local_signed_distance(
point::SVector{3,T},
mesh::SimpleMesh,
tri_indices::Vector{Int}
) -> TCompute signed distance from point to nearest triangle in the local set.
This is the key performance optimization: instead of checking all M triangles, we only check the k≈10-50 triangles in the point's octree leaf.
Performance: Fused computation avoids redundant closest_point calculation.
WhatsThePoint._compute_signed_distance_octree — Method
_compute_signed_distance_octree(point, mesh, tree) -> TCompute signed distance to the closest triangle using octree branch-and-bound.
This avoids global O(M) triangle scans by searching only relevant octree regions.
WhatsThePoint._create_root_octree — Method
_create_root_octree(::Type{T}, mesh::SimpleMesh, n_triangles::Int) where {T} -> SpatialOctree{Int,T}Create root octree covering mesh bounding box with all triangles in root.
WhatsThePoint._edge_key — Method
_edge_key(v1::SVector{3,T}, v2::SVector{3,T}) where {T} -> TupleCreate a canonical edge key from two vertices (order-independent). Uses component-wise comparison for consistent ordering.
WhatsThePoint._extract_vertex — Method
_extract_vertex(::Type{T}, vert) where {T} -> SVector{3,T}Extract coordinates from a Meshes.jl vertex to an SVector{3,T}.
WhatsThePoint._get_triangle_normal — Method
_get_triangle_normal(::Type{T}, mesh::SimpleMesh, tri_idx::Int) where {T} -> SVector{3,T}Extract and normalize triangle normal from mesh. Accesses mesh data on-the-fly.
WhatsThePoint._get_triangle_vertices — Method
_get_triangle_vertices(::Type{T}, mesh::SimpleMesh, tri_idx::Int) where {T} -> (SVector{3,T}, SVector{3,T}, SVector{3,T})Extract triangle vertices from mesh as SVectors. Accesses mesh data on-the-fly.
WhatsThePoint._normalize_normal — Method
_normalize_normal(::Type{T}, n_vec) where {T} -> SVector{3,T}Extract and normalize a Meshes.jl Vec normal to a unit SVector{3,T}.
WhatsThePoint._subdivide_triangle_octree! — Method
_subdivide_triangle_octree!(tree, mesh, box_idx, criterion)Recursively subdivide a box in the triangle octree.
For each box that needs subdivision:
- Create 8 child boxes
- For each triangle in parent, find which children it intersects
- Distribute triangle indices to children
- Recursively subdivide children if they meet criteria
WhatsThePoint._triangle_axis_test — Method
_triangle_axis_test(
axis::SVector{3,T},
v0::SVector{3,T},
v1::SVector{3,T},
v2::SVector{3,T},
half::SVector{3,T}
) where {T<:Real} -> BoolInternal helper for triangle-box intersection separating axis test.
Tests if the projection intervals of the triangle vertices and box overlap along the given axis.
Arguments
axis: Separating axis directionv0, v1, v2: Triangle vertices in box-centered coordinateshalf: Box half-extents
Returns
true if intervals overlap (potential intersection), false if separated
WhatsThePoint.add_box! — Method
add_box!(octree::SpatialOctree, i::Int, j::Int, k::Int, N::Int, parent_idx::Int) -> IntAdd new box to octree. Returns box index.
Automatically grows arrays if capacity exceeded.
Arguments
i, j, k: Integer coordinates at level NN: Refinement level (N=1 is root, N=2 is first subdivision, etc.)parent_idx: Index of parent box
Returns
Index of newly created box
WhatsThePoint.all_boxes — Method
all_boxes(octree::SpatialOctree) -> Vector{Int}Return indices of all boxes (leaves and internal nodes).
WhatsThePoint.all_leaves — Method
all_leaves(octree::SpatialOctree) -> Vector{Int}Return indices of all leaf boxes.
WhatsThePoint.balance_octree! — Method
balance_octree!(octree::SpatialOctree, criterion::SubdivisionCriterion)Enforce 2:1 balance constraint across entire octree.
Iteratively subdivides boxes that violate the 2:1 constraint until all adjacent boxes differ by at most one refinement level.
Arguments
criterion: Subdivision criterion (only size constraints are enforced)
Algorithm
- Collect all leaves
- Check each leaf for balance violations
- Subdivide violating neighbors (only respecting physical limits like h_min)
- Repeat until no violations
Note
- Uses
can_subdivide(notshould_subdivide) to ignore element count criteria - Balancing is a geometric constraint, not an optimization decision
- Maximum iterations limit prevents infinite loops. If hit, tree may not be fully balanced.
WhatsThePoint.bounding_box — Method
bounding_box(octree::SpatialOctree) -> (SVector{3}, SVector{3})Get overall bounding box of tree (root box).
WhatsThePoint.box_bounds — Method
box_bounds(octree::SpatialOctree, box_idx::Int) -> (SVector{3}, SVector{3})Get (mincorner, maxcorner) of box.
Returns
Tuple of (mincorner, maxcorner) as SVector{3,C}
WhatsThePoint.box_center — Method
box_center(octree::SpatialOctree, box_idx::Int) -> SVector{3}Compute spatial center of box using (i,j,k,N) coordinates.
Formula
center = origin + (2*[i,j,k] + 1) * box_size / 2where box_size = root_size / N
WhatsThePoint.box_size — Function
box_size(tree::AbstractSpatialTree, box_idx::Int) -> RealGet edge length of box.
Required
Trees must implement this function.
WhatsThePoint.box_size — Method
box_size(octree::SpatialOctree, box_idx::Int) -> RealGet edge length of box.
Box size at refinement level N is: root_size / N
WhatsThePoint.can_subdivide — Function
can_subdivide(criterion::SubdivisionCriterion, tree, box_idx) -> BoolCheck if box CAN be subdivided based on physical constraints only.
Unlike should_subdivide, this ignores content-based criteria (like element count) and only checks physical limits (like minimum size). Used during balancing where subdivision is required for geometric correctness, not optimization.
Arguments
criterion: Subdivision criterion (only size constraints are checked)tree: Spatial treebox_idx: Index of box to check
Returns
true if box can physically be subdivided, false if at minimum size limit.
Example
# For balancing, we only respect size limits
if needs_balancing(leaf)
if can_subdivide(criterion, tree, leaf)
subdivide!(tree, leaf)
end
endWhatsThePoint.closest_point_on_triangle — Method
closest_point_on_triangle(
P::SVector{3,T},
v1::SVector{3,T},
v2::SVector{3,T},
v3::SVector{3,T}
) where {T<:Real} -> SVector{3,T}Compute the closest point on triangle (v1, v2, v3) to point P.
Uses barycentric coordinate method from Ericson's "Real-Time Collision Detection". The closest point is computed by:
- Projecting P onto the triangle plane
- Computing barycentric coordinates
- Clamping to triangle if outside
Algorithm
The triangle can be parameterized as: T(u,v) = v1 + u(v2-v1) + v(v3-v1) for u,v ≥ 0, u+v ≤ 1
We find the closest point by solving a constrained minimization problem.
Returns
Point on triangle surface closest to P (may be on edge or vertex).
References
Ericson, "Real-Time Collision Detection", Chapter 5.1.5
WhatsThePoint.distance_point_triangle — Method
distance_point_triangle(
P::SVector{3,T},
v1::SVector{3,T},
v2::SVector{3,T},
v3::SVector{3,T}
) where {T<:Real} -> TCompute unsigned distance from point P to triangle (v1, v2, v3).
Returns
Unsigned distance (always positive or zero)
WhatsThePoint.distance_point_triangle — Method
distance_point_triangle(
P::SVector{3,T},
v1::SVector{3,T},
v2::SVector{3,T},
v3::SVector{3,T},
normal::SVector{3,T}
) where {T<:Real} -> TCompute signed distance from point P to triangle (v1, v2, v3).
The distance is:
- Positive if P is on the side of the triangle that the normal points to
- Negative if P is on the opposite side
- Zero if P is on the triangle plane
Algorithm
- Find closest point Q on triangle to P
- Compute distance ||P - Q||
- Determine sign based on which side of triangle P is on
Arguments
P: Query pointv1, v2, v3: Triangle vertices in counterclockwise ordernormal: Outward-pointing unit normal vector
Returns
Signed distance (positive = outside, negative = inside for closed surface)
Example
using StaticArrays
# Triangle in xy-plane
v1 = SVector(0.0, 0.0, 0.0)
v2 = SVector(1.0, 0.0, 0.0)
v3 = SVector(0.0, 1.0, 0.0)
normal = SVector(0.0, 0.0, 1.0)
# Point above triangle
P = SVector(0.25, 0.25, 1.0)
d = distance_point_triangle(P, v1, v2, v3, normal)
# d ≈ 1.0 (positive, on normal side)WhatsThePoint.find_boxes_at_coords — Method
find_boxes_at_coords(octree::SpatialOctree, i_target::Int, j_target::Int, k_target::Int, N_target::Int) -> Vector{Int}Find box(es) at given (i,j,k,N) coordinates.
- If exact match found at level Ntarget, returns [boxidx]
- If location is covered by coarser box, returns [coarserboxidx]
- If location is subdivided finer, returns all descendants at that location
Returns
Vector of box indices covering the target location
WhatsThePoint.find_leaf — Method
find_leaf(octree::SpatialOctree, point::SVector{3}) -> IntFind leaf box containing point. Returns box index.
Traverses tree from root to leaf in O(log n) time.
Arguments
point: Query point in same coordinate system as octree
Returns
Index of leaf box containing point
Throws
AssertionError if point is outside octree bounds
WhatsThePoint.find_neighbor — Method
find_neighbor(octree::SpatialOctree, box_idx::Int, direction::Int) -> Vector{Int}Find neighbor(s) in given direction. Returns vector of neighbor indices.
Handles 2:1 refinement level difference:
- If neighbor exists at same level: returns [neighbor_idx]
- If neighbor is subdivided (finer): returns children on shared face (up to 4)
- If neighbor doesn't exist (boundary): returns empty vector
Arguments
box_idx: Box to find neighbor ofdirection: Direction code (1-6, seeneighbor_direction)
Returns
Vector of neighbor box indices (may be empty if at boundary)
WhatsThePoint.has_children — Function
has_children(tree::AbstractSpatialTree, box_idx::Int) -> BoolCheck if box has been subdivided.
Required
Trees must implement this function.
WhatsThePoint.has_children — Method
has_children(octree::SpatialOctree, box_idx::Int) -> BoolCheck if box has been subdivided.
WhatsThePoint.is_leaf — Function
is_leaf(tree::AbstractSpatialTree, box_idx::Int) -> BoolCheck if box is a leaf (has no children).
Required
Trees must implement this function.
WhatsThePoint.is_leaf — Method
is_leaf(octree::SpatialOctree, box_idx::Int) -> BoolCheck if box has no children.
WhatsThePoint.needs_balancing — Method
needs_balancing(octree::SpatialOctree, box_idx::Int) -> BoolCheck if subdividing this box would violate 2:1 balance with any neighbor.
Returns true if any neighbor has grandchildren (2-level refinement difference).
WhatsThePoint.neighbor_direction — Method
neighbor_direction(direction::Int) -> (Int, Int, Int)Convert direction code to (di, dj, dk) offset.
Direction Codes
- 1: -x (left)
- 2: +x (right)
- 3: -y (bottom)
- 4: +y (top)
- 5: -z (front)
- 6: +z (back)
WhatsThePoint.num_elements — Method
num_elements(octree::SpatialOctree) -> IntTotal number of elements stored in tree.
WhatsThePoint.should_subdivide — Function
should_subdivide(criterion::SubdivisionCriterion, tree, box_idx) -> BoolDetermine if box should be subdivided based on criterion.
Arguments
criterion: Subdivision criterion to evaluatetree: Spatial treebox_idx: Index of box to check
Returns
true if box should be subdivided, false otherwise.
WhatsThePoint.subdivide! — Method
subdivide!(octree::SpatialOctree, box_idx::Int) -> SVector{8,Int}Subdivide box into 8 children. Returns child indices [1:8].
Child Ordering (Standard Octree Convention)
1: (0,0,0) - bottom-left-front (x-, y-, z-) 2: (1,0,0) - bottom-right-front (x+, y-, z-) 3: (0,1,0) - top-left-front (x-, y+, z-) 4: (1,1,0) - top-right-front (x+, y+, z-) 5: (0,0,1) - bottom-left-back (x-, y-, z+) 6: (1,0,1) - bottom-right-back (x+, y-, z+) 7: (0,1,1) - top-left-back (x-, y+, z+) 8: (1,1,1) - top-right-back (x+, y+, z+)
Arguments
box_idx: Index of box to subdivide (must be leaf)
Returns
SVector{8,Int} of child indices in standard order
WhatsThePoint.triangle_box_intersection — Method
triangle_box_intersection(
v1::SVector{3,T},
v2::SVector{3,T},
v3::SVector{3,T},
box_min::SVector{3,T},
box_max::SVector{3,T}
) where {T<:Real} -> BoolTest if triangle (v1, v2, v3) intersects axis-aligned box.
Uses the Separating Axis Theorem (SAT) with 13 potential separating axes:
- 3 box face normals (x, y, z axes)
- 1 triangle normal
- 9 edge-edge cross products
If any axis separates the triangle and box, they don't intersect.
Algorithm
- Translate triangle and box so box is centered at origin
- Test each potential separating axis
- Return false if any axis separates, true otherwise
References
- Akenine-Möller, "Fast 3D Triangle-Box Overlap Testing" (2001)
- Ericson, "Real-Time Collision Detection", Chapter 5.2.9
Performance
Optimized with early-out tests. Average case is much faster than worst case.