v0.3.0
Half-edge–primary mesh processing
The half-edge is now the working representation and faces is a derived
snapshot, so a multi-stage pipeline pays one HalfMesh::Build instead of one
per family transition. On a 5 M-face mesh a native clean (spurious → spikes →
holes → unref) performs exactly one build and one face harvest; tests/perf
asserts both counts.
- Representation contract —
Meshhas exactly three valid states
(arrays-only, half-edge-only, both-and-consistent), enforced by
ValidateInvariants(), withInvalidateFaces()/SyncFaces()/
InvalidateHalfMesh()and aBeginHalfEdgePipeline()scope that defers
per-stage snapshots to one final harvest. - New
HalfMeshprimitives —FRemoveBulk,FAddDisk,
VRemoveUnreferenced;FAddaccepts isolated corners and unwinds through an
undo log, so a rejected add leaves every array byte-identical. - Native repair/hole stages — the whole repair, spike, hole-closing and
unreferenced-vertex family mutates the live half-edge instead of clearing it. SimplifyandRemeshIsotropicread topology fromhalfMeshonly.
Performance
HalfMesh::Build pairs twins through a flat open-addressing table keyed by a
packed (min,max) vertex pair instead of std::unordered_map: 3.24× faster
(0.608 s vs. 1.969 s) at 495.8 MiB vs. 904.2 MiB peak on a 5,003,552-face mesh
(Release, MSVC 14.51, x64, i7-13700KF). Half-edge numbering, rejection behavior
and goldens are unchanged.
New
RemoveSpuriousComponents(factor),RemoveSpikes(maxIterations),
RemoveVerticesAndFill(vertexRemoves),RemoveFacesHalfEdge,
ComputeMeanEdgeLength.RectPacking.h—PackRectangles/EstimateSquareTextureSize: the atlas
packer's two-tier skyline+shelf core over integer pixel rectangles, with no
mesh involved.HalfMesh::BuildCount()/FFacesCount()so a pipeline can assert it
rebuilds connectivity once.Mesh::vertexNormals— authored per-vertex normals, transported rather
than maintained. Parallel toverticesunder the same contract as
vertexColors, but explicitly not a cache: nothing recomputes it, so an
empty array means "derive them yourself". It is remapped through every
operation that only renumbers vertices and dropped by every operation that
moves one. The openMVS bridge now carries normals in both directions, so a
round trip no longer replaces an artist's normals with geometric ones.BakeOntoAtlas(source, target, params)—RebakeTexture's counterpart
for a target whose UV layout is authored and must be preserved: it reads
target.faceTexcoordsinstead of generating a new atlas, so only the texels
change.BakeParams::faceMask— restrict rasterization to selected target faces.
Under a mask each page is seeded fromtarget.texturesDiffuserather than
black, so unselected texels keep what they had and the bake is a true
in-place edit of one region of an existing texture.
Breaking changes
CloseHoleschanged meaning — the first parameter is nowmaxHoleEdges
(default 30), a size threshold, not the oldnCloseHoles = 200count.
A scanned surface's large open boundary now stays open by default.vertexColorsis a contractual parallel array — empty, or exactly as long
asvertices, enforced byValidateInvariants().faceTexblobsisstd::vector<Mesh::TexIndex>(uint8), matching openMVS's
MVS::Mesh::TexIndex;MAX_TEXBLOBSis 255. The PLY on-disc property stays
int32, so existing files are unaffected.SaveGLTFtakesimageFormatandembedImages(defaulted to the previous
embedded-JPEG behavior).- glTF save → load is now an identity.
LoadGLTFpreviously applied
SaveGLTF's z-up → y-up root-node matrix like any other node transform and
returned the mesh rotated 90° about X, so the library could not read back what
it had just written. It now converts back to z-up after flattening the node
hierarchy. The rotation stays in the file, so exports remain upright in
Blender, three.js and Cesium. halfmesh is z-up in memory, its glTF files are
y-up is now a stated, fixed contract rather than an implied one. A y-up glTF
from any exporter loads correctly; a glTF carrying z-up data under an identity
node — non-conformant, but emitted by some writers, including openMVS's own
pre-halfmesh exporter — loads rotated and must be corrected by its producer.
PLY is unaffected. - A caller that hand-edits the public
facesarray must call
InvalidateHalfMesh()— the old count heuristic is gone. - Not ABI-compatible with 0.2.0 — recompile against the new headers.
Also
- openMVS interop gained consuming
ConvertMeshoverloads (rvalue source), so a
conversion peaks at the larger mesh plus one array rather than both at once. - vcpkg
builtin-baselineadvanced to0ac8df3b; thevcpkg-overlay-ports/
tinygltf hotfix is gone — a cold-cache build resolves everything from the
registry. LoadPLYkept the previous mesh's optional arrays whenever the new file
carried none, leaving them the wrong length against the new vertex and face
counts; both loaders now reset the optional set up front.- tinygltf is now built with
TINYGLTF_NOEXCEPTION/JSON_NOEXCEPTION, so glTF
JSON parsing no longer needs exceptions. OpenCV and tinyply still throw, so the
library as a whole is not yet buildable with exceptions disabled. - glTF textures are decoded and encoded with OpenCV
imgcodecsinstead of the
bundled stb codecs (TINYGLTF_NO_STB_IMAGE/_WRITE) — one image path for PLY
sidecars and glTF textures alike, and no stb compiled into the build at all.
JPEG export keeps the quality stb hardcoded, so file output is unchanged.
Full detail: CHANGELOG.md.
Python wheels
manylinux_2_28 x86_64 wheels for CPython 3.10–3.13 are attached below:
pip install https://github.com/cdcseacave/halfmesh/releases/download/v0.3.0/halfmesh-0.3.0-cp312-cp312-manylinux_2_28_x86_64.whl