Skip to content

UV: Slice D — Seams, sewing, and selection-scoped re-unwrap (#462) - #775

Merged
fernandotonon merged 5 commits into
masterfrom
feature/uv-seams-462
Jun 28, 2026
Merged

UV: Slice D — Seams, sewing, and selection-scoped re-unwrap (#462)#775
fernandotonon merged 5 commits into
masterfrom
feature/uv-seams-462

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds seam and pin attributes on EditableMesh with Ogre mesh UOB persistence (qtme.seams.*, qtme.uv_pins.*).
  • Implements sew/split UV ops, selection-scoped xatlas unwrap (respects seams + pins), and full undo via UvSeamCommands.
  • Wires Edit Mode seam marking (3D overlay + toolbar), UV panel tools (pin/unpin/sew/split/unwrap), and moves the UV editor to Material Mode → Mode Tools with a detached editor window.

Test plan

  • UnitTests --gtest_filter="UvSeam*:UvUnwrap*" (22 tests pass locally)
  • Edit Mode: mark seam on edge → split → pin → unwrap selected → sew → undo chain (no crash)
  • Open UV Editor window from Material Mode; canvas fits to view on open
  • Seam/pin state survives save/reload within the editor session (Ogre bindings)

Notes

  • FBX/glTF round-trip persistence for seams/pins is not included in this slice — data lives in Ogre mesh bindings only. Documented as follow-up if reviewers want it in this PR.

Closes #462

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added a dedicated UV editing workflow with a detached editor window and an embedded inspector view.
    • Introduced seam and pin tools, including mark/clear seam, pin/unpin, sew, split, and unwrap actions.
    • Added UV seam and pinned-vertex visualization in the editor.
  • Bug Fixes

    • UV edits now persist more reliably when switching between the properties panel and the full editor window.
    • The Material Editor section now starts collapsed for a cleaner panel layout.

Add seam/pin data on EditableMesh with Ogre mesh bindings, Edit Mode and
UV panel tools (mark, pin, sew, split, unwrap selected), undo support,
and a detached UV editor window under Material Mode tools.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fernandotonon, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 9 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e5815c6-8529-4ecb-9bfc-7964078d4fb4

📥 Commits

Reviewing files that changed from the base of the PR and between b9b3fa5 and 443b135.

📒 Files selected for processing (13)
  • qml/PropertiesPanel.qml
  • qml/UVEditorPanel.qml
  • src/EditModeController.cpp
  • src/EditableMesh.cpp
  • src/UVEditorController.cpp
  • src/UVEditorController.h
  • src/UvSeamData.cpp
  • src/UvSeamData_test.cpp
  • src/UvSeamOps.cpp
  • src/UvSeamOps_test.cpp
  • src/UvUnwrap.cpp
  • src/UvUnwrap_test.cpp
  • src/commands/UvSeamCommands.cpp
📝 Walkthrough

Walkthrough

Implements UV Slice D: adds seam/pin persistence on EditableSubMesh, UvSeamData serialization via Ogre user-object bindings, UvSeamOps split/sew topology operations, partial face-mask and pinned-UV extensions to UvUnwrap, three undoable command classes, seam overlay rendering in EditModeController, a full suite of operations on UVEditorController, an embedded UV panel mode, a detached UVEditorWindow, and Properties Panel / MainWindow wiring.

Changes

UV Seam / Pin / Sew / Split / Unwrap (Slice D)

Layer / File(s) Summary
EditableSubMesh data model and UvSeamData persistence
src/EditableMesh.h, src/UvSeamData.h, src/UvSeamData.cpp, src/EditableMesh.cpp
EditableSubMesh gains seamEdges and pinnedVertices sets; UvSeamData namespace provides edge-key encoding plus writeBindingsToMesh/readBindingsFromMesh via Ogre::UserObjectBindings; hooked into loadFromMesh, commitToEntity, and resizeEntityBuffers.
UvSeamData unit tests
src/UvSeamData_test.cpp
Tests cover edge-key order-independence, global-to-local vertex/edge mapping, seam/pin round-trip on a submesh, and full Ogre mesh binding persistence round-trip.
UvSeamOps split/sew topology
src/UvSeamOps.h, src/UvSeamOps.cpp, src/UvSeamOps_test.cpp
splitEdges duplicates endpoint vertices and rewrites triangle indices to create UV island borders; sewEdges averages coincident UV coordinates and clears seam flags; localEdgeKeysFromGlobal maps global edge pairs to per-submesh local keys.
UvUnwrap partial face mask, seam-aware, pinned-UV extensions
src/UvUnwrap.h, src/UvUnwrap.cpp, src/UvUnwrap_test.cpp
UvUnwrapOptions extended with FaceMask, seamEdgeKeys, and pinnedUvs; runUnwrap skips fully-ignored submeshes, wires faceIgnoreData/faceMaterialData into xatlas; buildUnwrappedSubmesh conditionally preserves original or pinned UVs for non-unwrapped vertices.
Undo commands: UvSeamMarkCommand, UvPinCommand, UvSeamTopologyCommand
src/commands/UvSeamCommands.h, src/commands/UvSeamCommands.cpp, src/CMakeLists.txt, tests/CMakeLists.txt
Three QUndoCommand subclasses handle seam flag changes, pin state changes, and before/after submesh snapshots for topology edits; each syncs edit-mode mesh and refreshes the UV editor on undo/redo.
EditModeController seam mark/clear and 3D overlay
src/EditModeController.h, src/EditModeController.cpp
Adds markSeamOnSelection/clearSeamOnSelection QML invokables; creates an EditMode/SeamEdge material and m_overlaySeamEdges ManualObject drawing red line segments for each submesh's seam edges; tears down the overlay on destroy.
UVEditorController operations and editor window lifecycle
src/UVEditorController.h, src/UVEditorController.cpp
Adds openEditorWindow/closeEditorWindow (QQmlApplicationEngine + singleton registration + QML load + visibleChanged teardown), setInspectorEmbedded, and the full seam/pin/sew/split/unwrap operation set with undo push, breadcrumbs, mesh-revision bump, and refresh.
QML UV Editor panel embedded mode, seam/pin overlays, detached window
qml/UVEditorPanel.qml, qml/UVEditorWindow.qml, src/qml_resources.qrc
UVEditorPanel gains embedded property, compact toolbar layout, seam/pin cache properties, drawSeamEdges/drawPinnedVertices canvas passes, and a Seams tool row; UVEditorWindow is a new standalone Window wrapping the panel with deferred fit-to-view.
Properties Panel and MainWindow wiring
qml/PropertiesPanel.qml, src/mainwindow.cpp, src/mainwindow.h
Properties Panel adds a collapsed "UV Edit" section with uvEditComponent (embedded panel + operation buttons) in Material mode; MainWindow adds Mark/Clear UV Seam toolbar actions for edge selection mode and replaces the embedded UV dock with openEditorWindow() calls throughout.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • fernandotonon/QtMeshEditor#281: Established the EditModeController selection overlay infrastructure that this PR extends with seam-edge overlay rendering.
  • fernandotonon/QtMeshEditor#759: Directly modified qml/UVEditorPanel.qml's rendering pipeline; this PR extends the same canvas and cache mechanism with seam/pin overlays and the embedded mode.
  • fernandotonon/QtMeshEditor#760: Added selection caching to rebuildTriangleCache() in UVEditorPanel.qml; this PR adds seam/pin cache passes to the same function.

Poem

🐇 Hop, hop — I mark the seam!
Red lines glow on every edge's dream.
Pin the UV, sew it tight,
Split the island left and right.
Unwrap the mesh with xatlas grace —
Every polygon finds its place! ✂️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not meet the save/load persistence requirement for seams and pins; the description says FBX/glTF round-trip support is not included. Add seam/pin persistence through the existing custom property channels for FBX and glTF, document the format, and verify save/load round-trips.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change set around seams, sewing, and selected-face re-unwrap.
Description check ✅ Passed The description covers the summary, test plan, and notes, and is sufficiently complete despite missing some template subsections.
Out of Scope Changes check ✅ Passed The code changes align with the seam, pin, unwrap, undo, and UI goals and do not show clear unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/uv-seams-462

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85b31b7479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/UvUnwrap.cpp
Comment on lines +625 to +626
if (!faceIgnore.empty())
decl.faceIgnoreData = reinterpret_cast<const bool*>(faceIgnore.data());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve UVs for faces outside the selection

When unwrapSelectedFaces() passes a mask with only some triangles selected, setting faceIgnoreData here does not preserve the skipped triangles' existing UVs: no current UV stream is provided to xatlas, and the code later replaces the entire submesh vertex/index buffers with xatlas output. As a result, unselected faces in the same submesh can lose or change their UVs even though this is advertised as selection-scoped re-unwrap; splice generated UVs only into included triangles or explicitly copy old UVs back for ignored faces.

Useful? React with 👍 / 👎.

Comment thread src/UvSeamOps.cpp Outdated
Comment on lines +171 to +173
const Ogre::Vector2 avg(
(sub.vertices[a].uv.x + sub.vertices[b].uv.x) * 0.5f,
(sub.vertices[a].uv.y + sub.vertices[b].uv.y) * 0.5f);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sew duplicate endpoints instead of collapsing the edge

For a normal selected UV edge (a,b), this averages the two edge endpoints together and then assigns that single midpoint UV to every vertex at either endpoint position. After splitting an edge, selecting one side and sewing it will collapse both ends of the edge to one UV point rather than pairing coincident duplicates per endpoint, corrupting the UV layout; sew should average duplicates at pa separately from duplicates at pb.

Useful? React with 👍 / 👎.

Comment on lines +71 to +75
if (auto* uv = UVEditorController::instance()) {
if (auto* wm = uv->workingMeshForEntity(entity))
return wm;
}
return nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load the entity mesh when no editor view is active

Seam/pin/topology undo commands become no-ops if the user changes modes or closes the UV editor before undoing, because meshForEntity() only returns the edit-mode mesh or the UV editor working mesh and otherwise returns nullptr. In that scenario the undo stack still advances but the seam/pin/topology change remains on the Ogre mesh; mirror UVEditCommand's fallback by loading a temporary EditableMesh from the entity and committing it back.

Useful? React with 👍 / 👎.

fernandotonon and others added 2 commits June 28, 2026 13:42
Keep both UV seam mark/clear APIs and the async select-by-part segmentation
from master.

Co-authored-by: Cursor <cursoragent@cursor.com>
Destroy the seam overlay ManualObject in destroySelectionOverlay so
Edit Mode tests can re-enter without duplicate-name crashes. Sew only
the selected edge endpoints, load EditableMesh from entity for undo when
UV/edit controllers are inactive, and preserve source UVs on verts
outside a partial unwrap selection.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

Addressed the Codex review items in b9b3fa5:

  • Seam overlay CI crash: destroySelectionOverlay() now destroys EditMode_SeamOverlay so repeated Edit Mode test setup doesn't hit duplicate ManualObject names.
  • Sew endpoint collapse: sew averages UVs only on the two edge endpoint verts (not all position-coincident verts).
  • Undo with inactive UV/edit controllers: seam/pin/topology undo loads an EditableMesh from the entity when no live controller mesh is available (same pattern as UVEditCommand).
  • Partial unwrap UV preservation: verts that appear only in ignored triangles keep their source UV channel instead of receiving xatlas output.

FBX/glTF round-trip for seams/pins remains out of scope for this slice (Ogre UOB bindings only), as noted in the PR description.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (4)
src/EditableMesh.cpp (1)

418-419: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add breadcrumbs around seam-binding load/persist paths.

These new hooks mutate persistent mesh state during load/commit/resize, but they currently leave no breadcrumb trail when seam/pin state is rehydrated or flushed back to Ogre. As per coding guidelines, src/**/*.{cpp,h} must “Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb(category, message)` using the established breadcrumb categories.

Also applies to: 809-809, 1374-1374

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/EditableMesh.cpp` around lines 418 - 419, The seam-binding load/persist
flow is missing breadcrumb tracking around significant state changes. Add
SentryReporter::addBreadcrumb calls in the EditableMesh paths that rehydrate or
flush seam/pin state, including the UvSeamData::readBindingsFromMesh load path
and the corresponding commit/resize persistence hooks referenced by this change.
Use the established breadcrumb categories and include a clear message for each
operation so user-facing mesh-state mutations are traceable.

Source: Coding guidelines

src/UvSeamData_test.cpp (1)

67-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an integration test for the new EditableMesh hooks too.

This only round-trips through UvSeamData::writeBindingsToMesh() directly, so it will not catch regressions where EditableMesh::commitToEntity() or EditableMesh::resizeEntityBuffers() stop persisting seam/pin bindings. As per coding guidelines, src/**/*_test.cpp should “Add Google Test unit tests for new functionality.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/UvSeamData_test.cpp` around lines 67 - 84, The current test only verifies
direct round-tripping through UvSeamData::writeBindingsToMesh(), so it misses
regressions in the new EditableMesh persistence hooks. Extend UvSeamDataTest by
adding an integration test that exercises EditableMesh::commitToEntity() and/or
EditableMesh::resizeEntityBuffers() after calling setSeam and setPinned on a
submesh, then reloads the mesh and asserts the bindings still persist. Keep the
existing round-trip coverage, but add the new test around EditableMesh,
mesh.loadFromMesh(), and the seam/pin accessors to cover the hook path.

Source: Coding guidelines

src/UvUnwrap_test.cpp (1)

131-151: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Verify untouched faces, not just that unwrap succeeded.

This only checks report.applied and chartCount, so it will not catch regressions where ignored triangles are rewritten anyway. Seed the mesh with known UVs and assert that the six unselected triangles keep those coordinates after unwrap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/UvUnwrap_test.cpp` around lines 131 - 151, The UvUnwrapTest_partial test
only verifies that unwrapEntity succeeds and creates charts, so it misses
regressions where non-masked triangles get modified. Update the test around
UvUnwrap::unwrapEntity and UvUnwrapOptions::FaceMask to seed the mesh with known
UVs before unwrapping, then assert that the unselected triangles in the masked
submesh keep their original UV coordinates after the call while only the
selected triangles are changed.
src/UvSeamOps_test.cpp (1)

43-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise sewEdges() with a real selected edge.

This test uses makeEdgeKey(0, 2), but 0 and 2 are coincident duplicate vertices, not an edge the caller can produce. That blesses the current implementation rather than the public contract, so a real split→sew regression on edge (a, b) would still pass here. Build the seam via splitEdges() and then sew that same edge key back.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/UvSeamOps_test.cpp` around lines 43 - 70, The
UvSeamOpsTest::SewAveragesCoincidentUvPositions case is using makeEdgeKey(0, 2)
for coincident duplicate vertices instead of a real selected edge, so it does
not exercise the public sewEdges() contract. Update the test to create the seam
through splitEdges() and then call sewEdges() with that returned edge key, so
the test covers an actual split-to-sew flow and validates
UvSeamData::setSeam/isSeam behavior on a real edge.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qml/PropertiesPanel.qml`:
- Around line 2554-2558: The UV Edit panel keeps inspectorEmbedded enabled when
sectionVisible hides the outer section but the Loader remains active, so the
cleanup in the Loader’s Component.onDestruction never runs. Update the UV Edit
visibility handling in PropertiesPanel.qml to explicitly clear
UVEditorController.setInspectorEmbedded(false) whenever the UV Edit section is
hidden by mode/selection changes, using the existing UVEditorController and
Loader/Component.onCompleted/Component.onDestruction flow as the place to hook
the reset.

In `@qml/UVEditorPanel.qml`:
- Around line 651-653: The selection overlay in UVEditorPanel.qml is being
covered by the pin markers, so selected pinned vertices lose their selected
appearance. Update the draw order around drawSelection and drawPinnedVertices,
or adjust the pinned-vertex rendering so selected vertices remain visibly
selected when both states apply. Use the existing drawSeamEdges, drawSelection,
and drawPinnedVertices rendering flow to keep the selection state on top for
pinned vertices.

In `@src/commands/UvSeamCommands.cpp`:
- Around line 207-219: UvSeamTopologyCommand::undo() and
UvSeamTopologyCommand::redo() mutate the mesh but do not record a breadcrumb
like the seam/pin command paths do. Update these methods in
UvSeamTopologyCommand to add the same breadcrumb recording used elsewhere before
calling applyMesh(m_before) or applyMesh(m_after), while preserving the existing
m_firstRedo behavior in redo().
- Around line 92-110: The seam/pin apply helpers currently update the mesh and
refresh the UV editor, but they skip the normal edit-mode change notification
path. Update UvSeamMarkCommand::apply and the corresponding pin command helper
to invoke EditModeController::notifyMeshDataChanged() after syncEditMeshFrom(),
or refactor them to reuse commitUvMeshState() so the initial action and
undo/redo both notify observers consistently.

In `@src/EditModeController.cpp`:
- Around line 796-801: The mark/clear seam path updates the editable mesh and
entity data but leaves the UV editor’s working state stale, so sync the UV view
after seam changes. Update the seam-edit flow around
UVSeamData::writeBindingsToMesh, UndoManager::push with UvSeamMarkCommand, and
the related clear-seam path to refresh UVEditorController and call
refreshAfterUvEdit() whenever seams change. Keep the existing selection/mesh
notifications, but ensure the UV editor’s m_workingMesh and seam overlay/query
data are rebuilt immediately after these operations.

In `@src/UVEditorController.cpp`:
- Around line 1982-1990: The sew flow in UVEditorController::sewEdges() updates
seam state in m_workingMesh but never persists the changed qtme.seams.* bindings
to the entity mesh. After UvSeamOps::sewEdges() succeeds, write the updated seam
bindings back using the same persistence path used by the mark/clear, pin/unpin,
split, and undo helpers, then keep commitWorkingMeshUvs() and the
undo/breadcrumb logic as-is.
- Around line 752-764: In UVEditorController::syncWorkingMeshFromEntity, avoid
always calling m_workingMesh.loadFromEntity(m_activeEntity) when
EditModeController::instance()->isEditModeActive() and edit->editEntity()
matches the active entity, because edit->currentMesh() is the authoritative mesh
state in that case. Update the sync logic to prefer the existing
currentMesh/subMeshes data from EditModeController when edit mode owns the mesh,
and only fall back to loading from the entity when edit mode is inactive or not
targeting m_activeEntity; keep applyUvChannel and the submesh copy path aligned
with that source of truth.
- Around line 1996-2036: splitSelectedEdges() and unwrapSelectedFaces() update
the mesh but do not follow the controller’s normal change-notification path.
After the topology/UV edits succeed, bump m_meshRevision and emit
meshDataChanged() the same way the other mutators in UVEditorController do,
keeping the existing refresh() call if needed. Use the splitSelectedEdges() and
unwrapSelectedFaces() flows as the place to restore the missing notification so
downstream listeners are informed of the mesh change.

In `@src/UvSeamData_test.cpp`:
- Around line 69-70: The Ogre-dependent test setup currently skips execution
when tryInitOgre() fails, which hides regressions; update the test
initialization in UvSeamData_test.cpp to use ASSERT_TRUE(tryInitOgre()) instead
of GTEST_SKIP(), and also assert canLoadMeshFiles() where that prerequisite is
required so failures surface loudly. Use the existing test setup code around
tryInitOgre() to apply the change without altering the rest of the test body.

In `@src/UvSeamData.cpp`:
- Around line 130-144: readBindingsFromMesh currently accepts persisted seam/pin
local ids without validating them, so stale or corrupt bindings can repopulate
subMeshes[i].seamEdges and subMeshes[i].pinnedVertices with out-of-range
vertices. Update readBindingsFromMesh to filter the deserialized EdgeKeyList and
PinList against the valid local vertex range for subMeshes[i] before inserting,
using the existing seamsKey/pinsKey and subMeshes[i].vertices data to locate the
checks. Keep only bindings that resolve to vertices present in the current
submesh and discard invalid entries during reload.

In `@src/UvSeamOps.cpp`:
- Around line 91-95: The seam-duplication path in UvSeamOps::splitEdge / related
seam copy logic creates new vertices with copyVertex but does not preserve
pinned state, so the duplicated seam endpoints can become movable unexpectedly.
Update the vertex duplication flow for newA/newB to copy the source vertex’s pin
flag (and any other pin-related state tracked separately) from sub.vertices[a]
and sub.vertices[b] onto the newly pushed vertices, including the same fix in
the matching logic referenced by the second location.
- Around line 165-177: The sewEdges/sewOne path is using the passed edge
endpoints directly, which averages the wrong UVs and makes splitEdges() not
reversible. Update sewOne to resolve the duplicated seam-side vertices for the
mesh-edge key (a, b) before averaging, so it sews the two split copies rather
than collapsing the original edge; keep UvSeamData::setSeam in sync with the
actual paired vertices being rejoined.

In `@src/UvUnwrap.cpp`:
- Around line 149-165: buildIncludedSourceVerts() is too coarse because it marks
a whole source vertex as included whenever any connected triangle is selected,
which causes shared boundary vertices to pull ignored faces into
buildUnwrappedSubmesh(). Update the selection tracking so inclusion is per
source-face corner or per xatlas clone/xref rather than one boolean per source
vertex, and use that finer-grained mask when deciding which UVs to rewrite. Keep
the fix localized around buildIncludedSourceVerts() and the selection handling
in buildUnwrappedSubmesh().
- Around line 47-115: buildFaceMaterials() is doing an unnecessary O(T²)
neighbor prepass even when no seam partitioning is needed; update the function
to short-circuit the chart-building path when seamEdges is empty (or equivalent
no-seam case) so the default unwrap flow skips the pairwise triangle comparison
entirely. Make the change in buildFaceMaterials() and ensure the call site in
runUnwrap() still passes through the fast path for submeshes without seam data,
while preserving the existing chart logic when seamEdges is present.

---

Nitpick comments:
In `@src/EditableMesh.cpp`:
- Around line 418-419: The seam-binding load/persist flow is missing breadcrumb
tracking around significant state changes. Add SentryReporter::addBreadcrumb
calls in the EditableMesh paths that rehydrate or flush seam/pin state,
including the UvSeamData::readBindingsFromMesh load path and the corresponding
commit/resize persistence hooks referenced by this change. Use the established
breadcrumb categories and include a clear message for each operation so
user-facing mesh-state mutations are traceable.

In `@src/UvSeamData_test.cpp`:
- Around line 67-84: The current test only verifies direct round-tripping
through UvSeamData::writeBindingsToMesh(), so it misses regressions in the new
EditableMesh persistence hooks. Extend UvSeamDataTest by adding an integration
test that exercises EditableMesh::commitToEntity() and/or
EditableMesh::resizeEntityBuffers() after calling setSeam and setPinned on a
submesh, then reloads the mesh and asserts the bindings still persist. Keep the
existing round-trip coverage, but add the new test around EditableMesh,
mesh.loadFromMesh(), and the seam/pin accessors to cover the hook path.

In `@src/UvSeamOps_test.cpp`:
- Around line 43-70: The UvSeamOpsTest::SewAveragesCoincidentUvPositions case is
using makeEdgeKey(0, 2) for coincident duplicate vertices instead of a real
selected edge, so it does not exercise the public sewEdges() contract. Update
the test to create the seam through splitEdges() and then call sewEdges() with
that returned edge key, so the test covers an actual split-to-sew flow and
validates UvSeamData::setSeam/isSeam behavior on a real edge.

In `@src/UvUnwrap_test.cpp`:
- Around line 131-151: The UvUnwrapTest_partial test only verifies that
unwrapEntity succeeds and creates charts, so it misses regressions where
non-masked triangles get modified. Update the test around UvUnwrap::unwrapEntity
and UvUnwrapOptions::FaceMask to seed the mesh with known UVs before unwrapping,
then assert that the unselected triangles in the masked submesh keep their
original UV coordinates after the call while only the selected triangles are
changed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4245a6db-1570-4732-af38-cac59d3a8fc5

📥 Commits

Reviewing files that changed from the base of the PR and between fa29255 and b9b3fa5.

📒 Files selected for processing (25)
  • qml/PropertiesPanel.qml
  • qml/UVEditorPanel.qml
  • qml/UVEditorWindow.qml
  • src/CMakeLists.txt
  • src/EditModeController.cpp
  • src/EditModeController.h
  • src/EditableMesh.cpp
  • src/EditableMesh.h
  • src/UVEditorController.cpp
  • src/UVEditorController.h
  • src/UvSeamData.cpp
  • src/UvSeamData.h
  • src/UvSeamData_test.cpp
  • src/UvSeamOps.cpp
  • src/UvSeamOps.h
  • src/UvSeamOps_test.cpp
  • src/UvUnwrap.cpp
  • src/UvUnwrap.h
  • src/UvUnwrap_test.cpp
  • src/commands/UvSeamCommands.cpp
  • src/commands/UvSeamCommands.h
  • src/mainwindow.cpp
  • src/mainwindow.h
  • src/qml_resources.qrc
  • tests/CMakeLists.txt
💤 Files with no reviewable changes (1)
  • src/mainwindow.h

Comment thread qml/PropertiesPanel.qml
Comment thread qml/UVEditorPanel.qml
Comment thread src/commands/UvSeamCommands.cpp
Comment thread src/commands/UvSeamCommands.cpp
Comment thread src/EditModeController.cpp
Comment thread src/UvSeamData.cpp
Comment thread src/UvSeamOps.cpp
Comment thread src/UvSeamOps.cpp Outdated
Comment thread src/UvUnwrap.cpp
Comment thread src/UvUnwrap.cpp Outdated
fernandotonon and others added 2 commits June 28, 2026 15:21
Clear UV inspector embedded state when the section hides, fix draw order
for selected pins, sync edit/UV controllers on seam changes, persist seam
bindings after sew, skip seam chart prep when unused, restore ignored-tri
UVs per-triangle after partial unwrap, validate binding reload, and expand
unit test coverage for split/sew, commit persistence, and partial unwrap.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

Addressed the open review threads in 915086d:

Codex

  • Partial unwrap now restores source UVs per ignored triangle after xatlas commit
  • Sew averages coincident verts at each endpoint position separately (split→sew safe)
  • Undo loads EditableMesh from the entity when UV/edit controllers are inactive

CodeRabbit

  • UV Edit clears inspectorEmbedded when the section is hidden/collapsed
  • Selection draws above pin markers in the UV canvas
  • Seam/pin undo uses commitUvMeshState() + topology undo breadcrumbs
  • Edit Mode seam mark/clear syncs the UV editor working mesh
  • syncWorkingMeshFromEntity() prefers edit-mode mesh over entity reload
  • Sew persists qtme.seams.* bindings; split/unwrap emit meshDataChanged
  • Binding reload rejects out-of-range seam/pin entries; split copies pin state
  • Skip O(T²) seam chart prep when no seams are present
  • Tests: split→sew, commit persistence, invalid bindings, partial unwrap UV preservation

FBX/glTF round-trip for seams/pins remains deferred (Ogre UOB only), as noted in the PR description.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit ae98bd5 into master Jun 28, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the feature/uv-seams-462 branch June 28, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UV: Slice D — Seams, sewing, and selection-scoped re-unwrap

1 participant