/** * MarchingCubes — SDF-to-watertight-triangle-mesh via the classic marching cubes algorithm. * * ## Algorithm Overview * * Marching cubes (Lorensen & Cline, 1987) evaluates a scalar field at the 8 corners of each * axis-aligned voxel and produces zero, one or more triangles whose vertices lie on the cell * edges where the field changes sign. The full 256-case edge/triangle lookup tables here are * taken verbatim from Paul Bourke's public-domain `polygonise()` implementation * (http://paulbourke.net/geometry/polygonise/). * * ## SDF sign convention * * The SDF convention used throughout this codebase (matching SDFPointEvaluator) is: * - **negative inside** the solid * - **positive outside** the solid * * The iso-surface is the zero level-set. Triangles are wound so that the face normal * (v1-v0)×(v2-v0) points **outward** (away from the negative / interior region). * Bourke's tri-table uses the opposite convention (inside = distance > isoLevel), so * this implementation swaps i1 and i2 in every emitted triangle to correct the winding. * Verified: the signed divergence-theorem volume of a unit sphere is positive, confirming * outward winding. * * ## Cube vertex / edge numbering (Bourke convention) * * 7 ──────── 6 * /| /| * 4 ──────── 5 | Edges: * | 3 ──────|─ 2 0: v0–v1 4: v4–v5 8: v0–v4 * | / | / 1: v1–v2 5: v5–v6 9: v1–v5 * 0 ──────── 1 2: v2–v3 6: v6–v7 10: v2–v6 * 3: v3–v0 7: v7–v4 11: v3–v7 * * Corner offsets (dx,dy,dz): * 0=(0,0,0) 1=(1,0,0) 2=(1,1,0) 3=(0,1,0) * 4=(0,0,1) 5=(1,0,1) 6=(1,1,1) 7=(0,1,1) * * ## Vertex placement * * Vertex positions are computed by **linear interpolation** along each cell edge at the * fraction where the SDF crosses `isoLevel`: * * t = (isoLevel - d0) / (d1 - d0) * vertex = p0 + t * (p1 - p0) * * ## Vertex welding * * Vertices are keyed by a canonical global grid-edge identifier: * `(lowerCornerFlatIndex) * 3 + dir` * where `lowerCornerFlatIndex = gx + nx*(gy + ny*gz)` for the lower-indexed corner of the * edge, and `dir` is 0=X, 1=Y, 2=Z. Because this key is intrinsic to the physical grid * edge (independent of which cell looks it up), adjacent cells that share an edge always * retrieve the same pre-computed vertex — giving an exactly-welded, watertight mesh with * zero floating-point tolerance required. * * ## Boundary handling (`closeBoundary`) * * If the SDF is still negative (inside) at the sampling-box faces the surface would exit * the grid and produce open holes. When `closeBoundary = true` (the default), boundary * layer sample values that are below `isoLevel` are clamped to `isoLevel + 1e-6`, forcing * the iso-surface to close at the box faces. * * **Trade-off**: the resulting caps are flat and lie exactly on the bounding-box faces. * For objects that genuinely extend beyond the sampled region this is geometrically * incorrect but watertight. Set `closeBoundary = false` if the object is known to be * fully contained within the sampling box (SDF is positive everywhere on the boundary). * * ## Units * * Geometry is unit-agnostic. STL convention is millimetres; the caller controls units * via the `bounds` option. **Never hardcode a unit conversion** (vault rule G.GOLD.485: * physical assumptions are config). Expose `scaleFactor` (default 1.0) to apply a * uniform scale to the output mesh without changing the SDF evaluation domain. * * @module MarchingCubes */ import type { SurfaceMesh } from '../AutoMesher'; import { type SDFNode, type SDFDistanceField } from '../SDFPointEvaluator'; /** * Options for {@link marchingCubes}. * * All fields are optional and have documented defaults. */ export interface MarchingCubesOptions { /** * World-space AABB to sample over. * Default: `{ min: [-1, -1, -1], max: [1, 1, 1] }`. * * STL convention is millimetres. The caller controls units by setting * these bounds appropriately — no unit conversion is ever hardcoded here * (G.GOLD.485: physical assumptions are config). */ bounds?: { min: readonly [number, number, number]; max: readonly [number, number, number]; }; /** * Number of sample points along each axis [nx, ny, nz]. * Higher values produce finer meshes at O(N³) cost. * Default: `[32, 32, 32]`. Must be >= 2 on every axis. */ resolution?: readonly [number, number, number]; /** * The iso-surface level. The mesh will be the level-set `SDF == isoLevel`. * Default: `0` (the zero level-set). */ isoLevel?: number; /** * When `true` (default), boundary face samples below `isoLevel` are clamped * to `isoLevel + 1e-6`, forcing the mesh to close at the box boundaries. * * Set to `false` only when the SDF is guaranteed positive everywhere on the * boundary faces (object fully contained in the sampling box). */ closeBoundary?: boolean; /** * Uniform scale applied to output vertex positions after meshing. * Does not affect SDF sampling or isoLevel comparison. * Default: `1.0`. * * Use to convert between unit systems without re-evaluating the SDF * (G.GOLD.485: physical assumptions are config, never hardcoded). */ scaleFactor?: number; } export interface MarchingCubesToleranceEstimate { /** Cell spacing between adjacent SDF samples, after scaleFactor. */ cellSize: [number, number, number]; /** Largest axis-aligned cell spacing. */ maxCellSize: number; /** 3D cell diagonal. */ cellDiagonal: number; /** Pessimistic surface-location envelope for tolerance gates. */ conservativeSurfaceError: number; /** The estimate uses the same unit system as bounds and mesh output. */ units: 'same-as-bounds'; } export interface ManufacturingBackendDecision { /** The backend this request should use for the requested tolerance. */ backend: 'sovereign-sdf-marching-cubes' | 'openscad-wasm'; /** True only when the mesh tolerance gate is tighter than this SDF sampling can prove. */ bridgeRequired: boolean; /** Requested mechanical tolerance in the same units as bounds/output, if supplied. */ requestedTolerance: number | null; /** Conservative mesh tolerance estimate used for the decision. */ tolerance: MarchingCubesToleranceEstimate; /** Human-readable reason suitable for receipts and API clients. */ reason: string; } /** * Estimate the meshing tolerance envelope implied by marching-cubes sampling. * * The estimate is deliberately conservative: it reports the full scaled cell * diagonal as the surface-location error gate. If a mechanical tolerance is * tighter than this value, the caller should increase resolution, shrink the * bounds around the part, or route to an exact CAD backend. */ export declare function estimateMarchingCubesTolerance(opts?: Pick): MarchingCubesToleranceEstimate; /** * Decide whether the sovereign SDF → Marching Cubes path is precise enough for * a requested mechanical tolerance, or whether an exact CAD backend should be * used instead. This intentionally does not claim an OpenSCAD bridge exists; it * only marks when such a bridge would be required by the tolerance gate. */ export declare function decideManufacturingBackend(opts?: Pick & { requestedTolerance?: number | null; }): ManufacturingBackendDecision; /** * Convert an SDF into a watertight triangle mesh using the standard 256-case * marching cubes algorithm with vertex welding. * * @param input - An {@link SDFNode} tree (sampled on demand) or a * pre-sampled {@link SDFDistanceField}. * @param opts - Sampling bounds, resolution, iso-level, and mesh options. * @returns - A {@link SurfaceMesh} with exactly-welded vertices and * outward-facing triangle normals. */ export declare function marchingCubes(input: SDFNode | SDFDistanceField, opts?: MarchingCubesOptions): SurfaceMesh; //# sourceMappingURL=MarchingCubes.d.ts.map