Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ if(OPENABF_MULTIHEADER)
include/OpenABF/HalfEdgeMesh.hpp
include/OpenABF/HierarchicalLSCM.hpp
include/OpenABF/AngleBasedLSCM.hpp
include/OpenABF/ChartPacking.hpp
include/OpenABF/MeshMerge.hpp
include/OpenABF/Math.hpp
include/OpenABF/Vec.hpp
include/OpenABF/MeshIO.hpp
Expand Down
1 change: 1 addition & 0 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ F2 or F5–F11 — based on priority at the time of starting.
| open | F11 | Implement ACVD | [#62](https://github.com/educelab/OpenABF/issues/62) | 2026-03-20 | 2026-03-20 |
| open | F2 | Multi-chart UV packing | [#18](https://github.com/educelab/OpenABF/issues/18) | 2026-03-13 | 2026-06-20 |
| open | B8 | LSCM area-preserving rescale of flattening output | [#98](https://github.com/educelab/OpenABF/issues/98) | 2026-06-20 | 2026-06-20 |
| open | B9 | No recoverable mapping from torn/parameterized mesh back to original input topology (insert_face rewind + split_path duplication) | [#100](https://github.com/educelab/OpenABF/issues/100) | 2026-06-20 | 2026-06-20 |

## Archived Tracks

Expand Down
45 changes: 45 additions & 0 deletions conductor/tracks/B9/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# B9 Implementation Plan

Make every torn/extracted vertex name its pre-HEM (input) vertex, so a consumer
who wraps their own mesh in a HalfEdgeMesh to flatten can build a per-wedge UV
map that lands back on their pre-HEM mesh — seam-duplicated corners included.
The `insert_face` winding reversal is absorbed by identity-keyed corner
resolution and needs no separate record (see spec.md and issue #100).

## Phase 1: Investigation & decision
- [ ] 1.1 Reproduce the gap: tear a seam, confirm the duplicate vertex has no
recorded link to its pre-HEM origin and that an atlas seam corner
cannot be resolved to the consumer's `(face, corner)` today
- [ ] 1.2 Confirm the already-recoverable hops to keep scope tight: face index
(insert_faces order + split_path never re-inserts), non-seam vertex
index (insert_vertices order + tearing only appends), and that
identity-keyed corner resolution absorbs the winding reversal
- [ ] 1.3 Survey duplication/copy sites: split_edge/split_path (duplication) and
clone_face_ (extraction vertex copy) — confirm an `origin` field rides
the copy path; choose vertex-origin tracking vs returned remaps and
record the decision in spec.md

## Phase 2: Tests (write first)
- [ ] 2.1 Duplicate → pre-HEM vertex recovery across split_path (incl. multi-seam)
- [ ] 2.2 Full round-trip: packed/merged atlas corner → F2 maps → torn-HEM corner
→ B9 mapping → pre-HEM face, corner, vertex; assert a seam corner lands
on the correct pre-HEM (face, corner) by identity
- [ ] 2.3 Mis-wound input face: confirm the winding reversal is absorbed — the
round-trip lands the correct corner with no corner-order record
- [ ] 2.4 Identity/no-op case: an untorn, correctly-wound mesh round-trips to the
identity mapping

## Phase 3: Implementation
- [ ] 3.1 Implement duplicate → pre-HEM vertex mapping in split_edge/split_path
(origin set at construction, copied to duplicates)
- [ ] 3.2 Ensure the mapping survives extract_connected_components (clone_face_
vertex copy) and composes with F2's vertex_map/vertex_source
- [ ] 3.3 Document behavior + recovery API on split_*; update the
MultiChartFlatten.cpp reference comment to key corners against the
pre-HEM mesh using the vertex origin
- [ ] 3.4 Regenerate single header (and update install list if a header is added)

## Phase 4: Verify
- [ ] 4.1 Run `ctest` — all suites pass
- [ ] 4.2 Run clang-format on changed files
- [ ] 4.3 Confirm single-header build compiles and runs
108 changes: 108 additions & 0 deletions conductor/tracks/B9/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# B9 — No recoverable mapping from torn seam-duplicate vertices back to the consumer's pre-HEM mesh

## GitHub Issue
https://github.com/educelab/OpenABF/issues/100

## Use case
Downstream consumers do **not** use `HalfEdgeMesh` as their primary mesh type.
They keep their own mesh (the "raw" / pre-HEM mesh — their own vertex array and
face list) and *wrap* it in a `HalfEdgeMesh` only to flatten: build the HEM with
`insert_vertices`/`insert_faces`, tear it (`split_path`), extract components,
parameterize, and pack/merge. They then build a **per-wedge UV map** from the
packed result and expect to apply it back onto their **pre-HEM mesh**, keyed by
their own `(face, corner)` identities.

## Summary
That round-trip is `atlas corner → (F2 maps) → torn-HEM corner → pre-HEM mesh`.
Three of the four hops already round-trip cleanly:

- **Face index.** `insert_faces` inserts faces in order
(`include/OpenABF/HalfEdgeMesh.hpp:1040-1049`) and `split_path` only tears
edges (it never re-inserts faces), so pre-HEM face `i` == HEM face `i`, and
F2's `face_map`/`face_source` carry it back.
- **Corner order.** `insert_face` may auto-reverse a mis-wound face to keep the
mesh manifold (`include/OpenABF/HalfEdgeMesh.hpp:1706-1748`), permuting a
face's stored corner order relative to the raw input. This is **not** a
blocker: the documented per-wedge recipe resolves corners by *vertex
identity* against the consumer's own face, never by raw traversal index
(`include/OpenABF/ChartPacking.hpp:108-123`), so the reversal is **absorbed**,
not inverted. (See "Out of scope".)
- **Non-seam vertices.** `insert_vertices` preserves order and tearing only
*appends* new vertices, so a non-duplicated HEM vertex keeps its pre-HEM
index; F2's `vertex_map`/`vertex_source` carry it back.

The one hop that breaks is **seam-vertex identity**. Tearing duplicates seam
vertices via `insert_vertex(oldStart->pos)`
(`include/OpenABF/HalfEdgeMesh.hpp:1445,1465`), appending a new index whose only
link to its origin is a copied position. `split_path`/`split_edge` return `void`
and record no `duplicate → original` vertex map. So a torn-HEM corner that lands
on a seam duplicate **cannot be expressed in the consumer's pre-HEM vertex
namespace** — and identity-based corner resolution against the consumer's own
face (the very mechanism that absorbs the winding reversal) fails for exactly
those corners.

## Why this matters
The per-wedge UV recipe in `MultiChartFlatten.cpp` works entirely *within* the
torn HEM's own namespace, which is self-consistent. But the consumer's goal is
to land UVs on their **pre-HEM** mesh. For every non-seam corner that already
works; for a seam corner it silently cannot, because the duplicate has no
recoverable pre-HEM vertex identity. The same gap blocks scattering per-vertex
attributes captured on the pre-HEM mesh and emitting output indexed by the
consumer's original vertices. Discovered during F2 (PR #99).

## Goal
Make every torn/extracted vertex — original or seam duplicate — name its
**pre-HEM (input) vertex**, so the documented identity-keyed per-wedge recipe
resolves corners against the consumer's own faces for *all* corners, seams
included. Composed with F2's `vertex_map`/`face_map` and
`vertex_source`/`face_source`, a consumer can take any corner of a packed/merged
atlas and name the pre-HEM face, corner, and vertex it came from.

## Out of scope
- Removing auto-rewinding or seam duplication (both are intentional).
- Recording the `insert_face` corner-order permutation / a reversed flag.
Identity-keyed corner resolution (`ChartPacking.hpp:108-123`) absorbs the
reversal, so it does **not** need to be inverted for this use case. The corner
order is recovered implicitly once seam-duplicate vertices carry their pre-HEM
identity (this AC), by locating each vertex within the consumer's own face.

## Acceptance Criteria
- [ ] `split_edge`/`split_path` expose a recoverable **duplicate → pre-HEM**
vertex mapping (e.g. an `origin` index stored on every vertex, set at
construction and copied to duplicates, that survives extraction; or a
returned/accumulated remap).
- [ ] The mapping survives `extract_connected_components` (`clone_face_` copies
vertices, so an `origin` field rides along) and composes with F2's
`vertex_map`/`vertex_source`.
- [ ] A worked path demonstrates the full round-trip: packed/merged atlas corner
→ (F2 maps) → torn-HEM corner → (B9 mapping) → pre-HEM face, corner, and
vertex index — resolving the corner by vertex identity against the
consumer's own face, with seam-duplicate corners resolving correctly.
- [ ] Unit tests on a mesh with at least one torn seam assert the duplicate →
pre-HEM vertex mapping recovers the original identity, and that a seam
corner of the packed atlas lands on the correct pre-HEM `(face, corner)`.
Include a mis-wound input face to confirm the winding reversal is absorbed
(the round-trip still lands the right corner without a corner-order record).
- [ ] An untorn, correctly-wound mesh round-trips to the identity mapping.
- [ ] `split_*` documentation describes the behavior and points to the recovery
API; the `MultiChartFlatten.cpp` reference comment is updated to key
corners against the pre-HEM mesh using the new vertex origin.
- [ ] Single-header regenerated; multiheader install list updated if a new
header is introduced.

## Candidate approaches (decide in Phase 1)
1. **Vertex origin tracking (primary).** Store an `origin` (pre-HEM vertex index)
set on construction and copied to duplicates by `split_edge`, so every vertex
— original or duplicate — names its pre-HEM vertex. Survives
`clone_face_`/extraction via the vertex copy path. With this, corners are
located by identity in the consumer's own face and the winding reversal needs
no separate record.
2. **Returned remaps.** `split_edge`/`split_path` return/accumulate
`duplicate → original` pairs. Lighter-weight but does not survive extraction
without the caller threading it through, and does not give a uniform
"every vertex names its input vertex" accessor.

## Dependencies
- Independent of F2 (PR #99), but motivated by it; the F2 maps
(`vertex_map`/`face_map`, `vertex_source`/`face_source`) are the downstream
half of the chain B9 completes back to the pre-HEM mesh.
68 changes: 51 additions & 17 deletions conductor/tracks/F2/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,59 @@ Design resolved 2026-06-20 (see spec.md → Design Decisions).
- [x] 1.6 Resolve edge cases: empty list no-op, zero-area placed, null/empty throws, Dim>=2

## Phase 2: Tests (write first)
- [ ] 2.1 Synthetic 2D charts: bbox computation correctness (min/max per mesh)
- [ ] 2.2 Assert packed chart bounding boxes do not overlap (padding respected)
- [ ] 2.3 Assert absolute-mode preserves relative chart sizes (no per-chart distortion)
- [ ] 2.4 Assert normalize=true fits all UVs within [0,1]² via single global scale
- [ ] 2.5 Assert returned PackResult extent bounds all packed charts
- [ ] 2.6 Degenerate cases: empty list, single chart, zero-area chart, null/empty throw
- [ ] 2.7 End-to-end: tear → extract_connected_components → LSCM → PackCharts, and
- [x] 2.1 Synthetic 2D charts: bbox computation correctness (min/max per mesh)
- [x] 2.2 Assert packed chart bounding boxes do not overlap (padding respected)
- [x] 2.3 Assert absolute-mode preserves relative chart sizes (no per-chart distortion)
- [x] 2.4 Assert normalize=true fits all UVs within [0,1]² via single global scale
- [x] 2.5 Assert returned PackResult extent bounds all packed charts
- [x] 2.6 Degenerate cases: empty list, single chart, zero-area chart, null/empty throw
- [x] 2.7 End-to-end: tear → extract_connected_components → LSCM → PackCharts, and
verify per-wedge recovery via (face_map[f], vertex_map[corner.vertex.idx])

## Phase 3: Implementation
- [ ] 3.1 Create `include/OpenABF/ChartPacking.hpp` with PackOptions, PackResult
- [ ] 3.2 Implement per-chart bbox + sqrt-area target width + shelf placement
- [ ] 3.3 Implement absolute (translate-only) and normalize (global uniform scale) modes
- [ ] 3.4 Implement padding, degenerate-input handling, static_assert(Dim>=2)
- [ ] 3.5 Document the vertex-identity per-wedge recipe in the header + complexity notes
- [ ] 3.6 Add include to `include/OpenABF/OpenABF.hpp`
- [ ] 3.7 Update `single_include.json` and run amalgamation script
- [x] 3.1 Create `include/OpenABF/ChartPacking.hpp` with PackOptions, PackResult
- [x] 3.2 Implement per-chart bbox + sqrt-area target width + shelf placement
- [x] 3.3 Implement absolute (translate-only) and normalize (global uniform scale) modes
- [x] 3.4 Implement padding, degenerate-input handling, static_assert(Dim>=2)
- [x] 3.5 Document the vertex-identity per-wedge recipe in the header + complexity notes
- [x] 3.6 Add include to `include/OpenABF/OpenABF.hpp`
- [x] 3.7 Update single-header via amalgamation script (single_include.json unchanged —
it already tracks OpenABF.hpp transitively)

## Phase 4: Verify
- [ ] 4.1 Run `ctest` — all tests pass
- [ ] 4.2 Run clang-format on changed files
- [ ] 4.3 Confirm single-header build matches multi-header
- [x] 4.1 Run `ctest` — all suites pass (incl. OpenABF_TestChartPacking, OpenABF_TestMeshMerge)
- [x] 4.2 Run clang-format on changed files
- [x] 4.3 Confirm single-header build compiles and runs

## Phase 5: MergeMeshes helper (added during review)
Rationale: the inline atlas merge in the example severs the back-map chain.
MergeMeshes is the inverse of extract_connected_components — it returns
provenance maps so merged → chart → M' composition keeps working.
- [x] 5.1 Tests first: concatenation counts, vertex/face provenance, null/empty
throw, round-trip extract→merge recovers original (torn-mesh) identity
- [x] 5.2 Implement `MergeMeshes<MeshType>` → `MergedMesh{mesh, vertex_source, face_source}`
in `include/OpenABF/MeshMerge.hpp` (preserves vertex traits/positions;
edge/face traits default-constructed)
- [x] 5.3 Wire into OpenABF.hpp + multiheader install list; regenerate single header
- [x] 5.4 Switch MultiChartFlatten example to use MergeMeshes
- [x] 5.5 Verify: full ctest, single-header build, install-test all pass

## Phase 6: Perimeter padding (added during review)
Rationale: review question "shouldn't we add padding around the packed
charts?". The original layout applied `padding` only as a gutter *between*
charts — perimeter charts still touched the atlas boundary (left/bottom at the
origin, rightmost/topmost at the extent). For a texture atlas this lets edge
charts bleed across the boundary/seam under filtering, mipmapping, or wrap
addressing. Resolution (user-confirmed): inset the whole layout so `padding`
surrounds every chart on all four sides; keep the library default `padding = 0`
and instead set a visible padding in the example.
- [x] 6.1 Tests first: assert padding insets charts from the atlas perimeter
(new PaddingSurroundsChartsAtPerimeter); update single-row extent
expectation (pad + w0 + pad + w1 + pad)
- [x] 6.2 Implement perimeter inset: cursor starts/wraps at `pad`; add `pad` to
far extents; normalize fits the padded atlas into [0,1]²
- [x] 6.3 Update header docs (padding surrounds charts; atlas lower corner stays
at origin) + spec Decision 5 / acceptance criteria
- [x] 6.4 Set a visible `padding` in the MultiChartFlatten example
- [x] 6.5 Regenerate single header; verify full ctest, example run, single-header
build, clang-format all pass
6 changes: 5 additions & 1 deletion conductor/tracks/F2/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ and emit per-corner `vt` entries, build atlases, etc., from there.
struct PackOptions {
bool normalize = false; // fit packed atlas into [0,1]^2
std::optional<T> target_width{}; // overrides sqrt-area heuristic
T padding = T(0); // per-chart gutter, absolute units
T padding = T(0); // gutter on all sides of every chart
// (incl. atlas perimeter), abs units
};
struct PackResult { Vec<T,2> min, max; }; // packed atlas extent

Expand All @@ -98,6 +99,9 @@ and emit per-corner `vt` entries, build atlases, etc., from there.
- [ ] Shelf-packing with the ~square target-width heuristic, overridable.
- [ ] No charts' bounding boxes overlap (padding respected); the packed set
is contained in the returned extent (and in `[0,1]²` when normalized).
- [ ] `padding` surrounds every chart on all four sides, including against the
atlas boundary (perimeter charts are inset from the extent by `padding`,
not just separated from neighbors).
- [ ] Edge cases handled per Design Decision 6.
- [ ] Header documents the vertex-identity per-wedge recipe (Decision 2).
- [ ] Tests: synthetic 2D charts for deterministic geometric assertions plus
Expand Down
Loading