feat(edit): subdivide + fill (Phase 4 topology ops)#324
Conversation
Implements the remaining Subdivide and Fill items from issue #259. HalfEdgeMesh: - subdivideFaces: 1-to-4 split with shared midpoints between selected faces and T-junction-aware retriangulation of adjacent triangles (1/2/3 split-edge cases all handled). - fillSelection: 3-N vertex fan-triangulation for vertex fills and closed boundary edge loops; refuses cross-submesh, duplicate-tri, and orphaned-only inputs. EditModeController: - subdivideSelection wires Face mode (selected tris) and Edge mode (incident tris of selected edges) to the HE primitive, captures midpoint positions to re-select after the round-trip pack, and pushes one EditMeshTopologyCommand for undo. - fillSelection wires Vertex mode (set order = fan order) and Edge mode (closed-loop detection via degree-2 walk). UI: - Two new toolbar buttons (Subdivide ⊞, Fill ◆) with mode-aware enable/visibility. - F shortcut fills in edit mode when the selection is fillable; otherwise falls through to the existing Frame-Selection behavior. Tests: - 15 new HalfEdgeMesh standalone tests covering both ops, including T-junction handling, UV interpolation, cross-submesh rejection, and round-trip safety. - 7 new controller tests guarded by tryInitOgre (skip on macOS, run on Linux CI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
addRTSSResources() registered up to 4 candidate directories, deduplicated only by canonical path. On macOS dev builds the bundle's interior media/ and the dev-copy at bin/media/ are distinct paths with identical contents, so both got registered. Ogre re-parses .program scripts per resource location, and the second pass over ShadowVolumeExtude.program raised ItemIdentityException on the duplicate Ogre/ShadowBlendVP GpuProgram, aborting startup. Pick the FIRST existing RTShaderLib + sibling Main pair and stop. Both copies hold the same files, so the order doesn't matter functionally — this just avoids the double-registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HalfEdgeMesh tests (+8):
- subdivide every cube face stays closed-manifold
- subdivide preserves blended bone weights at midpoints
- splitMask=2 path verified on the quad neighbor
- cross-submesh subdivide leaves submesh 1 untouched
- non-adjacent multi-face selection on a cube
- fill caps a deleted cube face back to closed
- fill accepts orphaned vertices (post-delete hole-cap flow)
- fill round-trip safe through toEditableMesh/buildFromEditableMesh
Controller tests (+3):
- subdivideSelection in edge mode promotes incident faces
- fillSelection in vertex mode produces a new triangle
- fillSelection pushes an undo command
Docs:
- CLAUDE.md keyboard shortcut line notes F's dual role and X/Ctrl+X
in Edit Mode
- New "Edit Mode (Phase 4 topology)" section summarising the
EditModeController + HalfEdgeMesh op set, with a targeted note on
Subdivide and Fill
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughTwo new topology-editing operations—Subdivide and Fill—are introduced to the Edit Mode system with implementations across HalfEdgeMesh for mesh mutation, EditModeController for orchestration, UI toolbar integration, and comprehensive test coverage. A minor RTShaderHelper fix is also included. Changes
Sequence DiagramsequenceDiagram
participant User
participant MainWindow as UI / Toolbar
participant EMC as EditModeController
participant HEM as HalfEdgeMesh
participant EM as EditableMesh
participant Undo as Undo System
User->>MainWindow: Click Subdivide / Press F
MainWindow->>EMC: subdivideSelection() / fillSelection()
EMC->>EMC: Resolve selection<br/>(faces or edge→face mapping)
EMC->>HEM: subdivideFaces(faceIndices)<br/>or fillSelection(vertexIndices)
HEM->>HEM: Compute midpoints / fan triangulation<br/>Rebuild topology (edges/twins/boundaries)
HEM-->>EMC: Return new vertex indices<br/>or triangle count
EMC->>EM: Write modified topology<br/>back into EditableMesh
EMC->>EMC: Recompute normals<br/>Resize Ogre buffers
EMC->>EMC: Rebuild selection<br/>(re-identify midpoints/vertices)
EMC->>Undo: Push EditMeshTopologyCommand<br/>with "Subdivide"/"Fill" label
EMC->>MainWindow: Emit selection/mesh<br/>change signals
MainWindow->>User: Update overlays & viewport
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acfceee98d
ℹ️ 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".
| // Every vertex on a closed loop has degree 2. | ||
| for (const auto& [v, nbrs] : adj) { | ||
| if (nbrs.size() != 2) return {}; |
There was a problem hiding this comment.
Require boundary edges before edge-mode fill
buildClosedEdgeLoop only validates graph degree/connectivity, so any closed cycle of selected edges is treated as fillable. In edge mode this allows filling loops that are not boundaries (for example, the perimeter of an already-existing face on a closed mesh), and HalfEdgeMesh::fillSelection will then append overlapping triangles, creating non-manifold/duplicated geometry instead of capping a hole. The loop construction needs a boundary-edge check against the live mesh (e.g., each selected edge has exactly one incident face) before calling fill.
Useful? React with 👍 / 👎.
| editCtrl->fillSelection(); | ||
| event->accept(); | ||
| return; |
There was a problem hiding this comment.
Fall back to frame-selection when fill operation is rejected
The F shortcut unconditionally accepts the event and returns whenever the selection count looks fillable, but it ignores whether fillSelection() actually succeeded. For invalid loops/rejected fills (e.g., 3 edges that do not form a closed boundary), this makes F do nothing instead of falling through to frame-selection as intended. Gate the early return on fillSelection() > 0 (or equivalent success check).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/RTShaderHelper.cpp (1)
93-112: 🛠️ Refactor suggestion | 🟠 MajorUse
macBundlePath()instead of manual../..traversal on macOS.This segment still hardcodes bundle-relative traversal from
applicationDirPath(), which is brittle across packaging layouts. Please switch macOS candidate roots tomacBundlePath()-based paths.As per coding guidelines: "Use macBundlePath() for macOS to get
.appbundle root (notContents/MacOS/). Ogre resource paths inresources.cfgresolve relative to this bundle root".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/RTShaderHelper.cpp` around lines 93 - 112, Replace the manual ../.. and ../../../ traversals in RTShaderHelper.cpp (where you build macOS candidates using QCoreApplication::applicationDirPath()) with macBundlePath() roots: instead of appDir + "/../../media/RTShaderLib" and appDir + "/../../../media/RTShaderLib" add candidates based off macBundlePath(), e.g. macBundlePath() + "/media/RTShaderLib" (bundle-contained media) and macBundlePath() + "/../media/RTShaderLib" (adjacent install/dev layout); keep the other non-mac candidates and the candidates list logic unchanged.
🧹 Nitpick comments (3)
src/EditModeController_test.cpp (1)
1703-1879: Edge-mode fill still needs a controller test.The new suite exercises vertex-mode fill, but it never hits the controller-only path that walks a selected boundary-edge loop into an ordered vertex ring. A regression there would still pass these tests and the lower-level
HalfEdgeMeshcoverage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/EditModeController_test.cpp` around lines 1703 - 1879, The test suite lacks a controller-level test for edge-mode fill (the code path that walks a selected boundary-edge loop into an ordered vertex ring), so add a new TEST_F (e.g., FillSelectionEdgeModeHandlesBoundaryLoop) that: obtains the singleton EditModeController::instance(), calls enterEditMode(), sets selection mode to EditModeController::EdgeMode, selects a real boundary edge via selectEdge(va,vb) using an edge pulled from extractEntityBuffers(m_entity, pos, tris) (like other tests do to pick a safe edge), asserts that ctrl->fillSelection() returns > 0 (or the expected triangle count change), and finally verifies the entity buffers via extractEntityBuffers to ensure triangles increased or the fill effect occurred; follow the patterns in FillSelectionVertexModeProducesNewTriangle and SubdivideSelectionEdgeModePromotesIncidentFaces for setup and validation.src/HalfEdgeMesh_test.cpp (1)
4406-4425: Rename this test or expand it to actually cover all split-mask branches.The current fixture only drives one neighbor split-mask case on the quad, so the title/comment overstates coverage for
splitMaskhandling. That makes the suite look stronger than it is.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/HalfEdgeMesh_test.cpp` around lines 4406 - 4425, The test named SubdivideFacesHandlesAllSplitMaskPaths (TEST(HalfEdgeMeshStandalone, SubdivideFacesHandlesAllSplitMaskPaths)) claims to exercise all splitMask branches but only drives one neighbor case; either rename the test to accurately reflect that it only tests a single splitMask path or expand it to cover all cases by creating fixtures that trigger splitMask values 1,2,4 and combinations (e.g., build multiple small meshes or adjust selections on makeQuadMesh/constructed HalfEdgeMesh so subdivideFaces(...) exercises neighbor edges that produce each splitMask branch), then assert expected face counts/structure and keep calls to he.subdivideFaces, he.validate, and activeFaceCount(he) for each scenario.src/EditModeController.cpp (1)
2993-3004: Tolerance inconsistency:1e-10fvs1e-8fused elsewhere.The position-matching tolerance here (
1e-10f) is 100× tighter thanfindGlobalVertsByPosition(1e-8f) and other similar lookups in this file. While tighter is generally safer for subdivision midpoints (which should be exact), the inconsistency could cause subtle mismatches if floating-point rounding occurs during the HE → EditableMesh round-trip.Consider using a named constant shared across all position-based vertex lookups for consistency.
♻️ Suggested constant
+namespace { +constexpr float kPositionMatchEps2 = 1e-8f; // squared distance tolerance for position-based vertex matching +} // namespace // In subdivideSelection and findGlobalVertsByPosition: -if (sub.vertices[li].position.squaredDistance(tgt) < 1e-10f) { +if (sub.vertices[li].position.squaredDistance(tgt) < kPositionMatchEps2) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/EditModeController.cpp` around lines 2993 - 3004, The tolerance 1e-10f used when comparing sub.vertices[li].position to midpointPositions is inconsistent with other lookups (e.g., findGlobalVertsByPosition) which use 1e-8f; replace the hardcoded 1e-10f with a shared named constant (e.g., POSITION_MATCH_TOLERANCE) declared alongside other position-lookup utilities so all position-based comparisons (including the loop that uses subs, midpointPositions and the m_selectedVertices insertion) use the same tolerance (set to 1e-8f) to avoid subtle mismatches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/HalfEdgeMesh.cpp`:
- Around line 4764-4789: The duplicate-triangle check currently only runs when n
== 3; update it to also guard against any triangle that the fan will emit for n
> 3 by checking, for each i in 1..n-2, whether the triangle (vertexIndices[0],
vertexIndices[i], vertexIndices[i+1]) already exists; use
facesAroundVertex(vertexIndices[0]) and faceVertices(f) to detect an identical
set of three vertex indices (order-insensitive) and return 0 if any would
duplicate an existing face before calling appendTriangle, keeping the existing
triCount/appendTriangle logic intact.
In `@src/mainwindow.cpp`:
- Around line 1341-1353: The F-key handling accepts the event whenever fillable
is true, but fillSelection() can return 0 on failure which still swallows the
frame-selection fallback; change the logic in the edit-mode branch (use
editCtrl, selectionMode, EditModeController::VertexMode/EdgeMode,
selectedVertexCount/selectedEdgeCount, fillSelection(), event->accept()) to call
fillSelection(), capture its return value, and only call
SentryReporter::addBreadcrumb, event->accept(), and return when fillSelection()
indicates success (non-zero); if fillSelection() returns 0, do not accept the
event so the normal frame-selection fallback can run.
---
Outside diff comments:
In `@src/RTShaderHelper.cpp`:
- Around line 93-112: Replace the manual ../.. and ../../../ traversals in
RTShaderHelper.cpp (where you build macOS candidates using
QCoreApplication::applicationDirPath()) with macBundlePath() roots: instead of
appDir + "/../../media/RTShaderLib" and appDir + "/../../../media/RTShaderLib"
add candidates based off macBundlePath(), e.g. macBundlePath() +
"/media/RTShaderLib" (bundle-contained media) and macBundlePath() +
"/../media/RTShaderLib" (adjacent install/dev layout); keep the other non-mac
candidates and the candidates list logic unchanged.
---
Nitpick comments:
In `@src/EditModeController_test.cpp`:
- Around line 1703-1879: The test suite lacks a controller-level test for
edge-mode fill (the code path that walks a selected boundary-edge loop into an
ordered vertex ring), so add a new TEST_F (e.g.,
FillSelectionEdgeModeHandlesBoundaryLoop) that: obtains the singleton
EditModeController::instance(), calls enterEditMode(), sets selection mode to
EditModeController::EdgeMode, selects a real boundary edge via selectEdge(va,vb)
using an edge pulled from extractEntityBuffers(m_entity, pos, tris) (like other
tests do to pick a safe edge), asserts that ctrl->fillSelection() returns > 0
(or the expected triangle count change), and finally verifies the entity buffers
via extractEntityBuffers to ensure triangles increased or the fill effect
occurred; follow the patterns in FillSelectionVertexModeProducesNewTriangle and
SubdivideSelectionEdgeModePromotesIncidentFaces for setup and validation.
In `@src/EditModeController.cpp`:
- Around line 2993-3004: The tolerance 1e-10f used when comparing
sub.vertices[li].position to midpointPositions is inconsistent with other
lookups (e.g., findGlobalVertsByPosition) which use 1e-8f; replace the hardcoded
1e-10f with a shared named constant (e.g., POSITION_MATCH_TOLERANCE) declared
alongside other position-lookup utilities so all position-based comparisons
(including the loop that uses subs, midpointPositions and the m_selectedVertices
insertion) use the same tolerance (set to 1e-8f) to avoid subtle mismatches.
In `@src/HalfEdgeMesh_test.cpp`:
- Around line 4406-4425: The test named SubdivideFacesHandlesAllSplitMaskPaths
(TEST(HalfEdgeMeshStandalone, SubdivideFacesHandlesAllSplitMaskPaths)) claims to
exercise all splitMask branches but only drives one neighbor case; either rename
the test to accurately reflect that it only tests a single splitMask path or
expand it to cover all cases by creating fixtures that trigger splitMask values
1,2,4 and combinations (e.g., build multiple small meshes or adjust selections
on makeQuadMesh/constructed HalfEdgeMesh so subdivideFaces(...) exercises
neighbor edges that produce each splitMask branch), then assert expected face
counts/structure and keep calls to he.subdivideFaces, he.validate, and
activeFaceCount(he) for each scenario.
🪄 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: 94499c0f-9449-45a0-b956-14cd823a03d8
📒 Files selected for processing (9)
CLAUDE.mdsrc/EditModeController.cppsrc/EditModeController.hsrc/EditModeController_test.cppsrc/HalfEdgeMesh.cppsrc/HalfEdgeMesh.hsrc/HalfEdgeMesh_test.cppsrc/RTShaderHelper.cppsrc/mainwindow.cpp
Three issues from the PR review:
1. Codex P1: buildClosedEdgeLoop only checked graph degree, so any
closed cycle of selected edges (e.g. the perimeter of an existing
face) passed through. Filling such a cycle stacked overlapping
triangles on the live geometry. Added a per-edge boundary check
(each selected edge must touch exactly one face) before the loop
walk, refusing interior-edge cycles upfront.
2. CodeRabbit Major: HalfEdgeMesh::fillSelection's duplicate-triangle
check was gated on n == 3, leaving n > 3 fans free to recreate
existing tris on their first sub-triangle. Replaced the gate with a
per-fan-triangle check: every (apex, v_i, v_{i+1}) tri is checked
against existing faces incident to the apex. Added a regression
test (FillSelectionRejectsLargerFanThatDuplicatesExistingTri).
3. Codex P2 / CodeRabbit Major: the F shortcut accepted the event
whenever the selection looked fillable, even when fillSelection()
returned 0 (e.g. interior edge loop after fix #1, or duplicate-
triangle rejection after fix #2). Now only swallows the keypress on
a non-zero return — so F still falls back to Frame-Selection when
the fill is rejected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|



Summary
EditModeController, the Edit Mode toolbar (⊞ Subdivide, ◆ Fill), and anFshortcut that fills when the selection is fillable and otherwise falls through to Frame-Selection.media/RTShaderLibregistration →ItemIdentityExceptiononOgre/ShadowBlendVP).Code paths
HalfEdgeMesh::subdivideFaces— shared midpoints between selected tris; adjacent unselected tris retriangulate via asplitMaskswitch (1/2/3 split-edge cases) so we never leak T-junctions.HalfEdgeMesh::fillSelection— submesh-consistent fan-triangulation; rejects cross-submesh inputs, duplicate-of-existing-tri inputs (vertex-set comparison), and out-of-range / duplicate vertex indices. Accepts orphaned vertices (post-deleteFaceshole-cap flow).EditModeController::subdivideSelection— Face mode subdivides selected tris; Edge mode promotes every triangle incident to a selected edge.EditModeController::fillSelection— Vertex mode fan-triangulates the selection; Edge mode runs a degree-2 walk to extract a closed boundary loop before filling.Tests
splitMaskcases, cross-submesh handling, hole-cap with orphaned verts, round-trip safety.Test plan
cmake --build build_local --target UnitTests— passesHalfEdgeMeshStandalonesuite (162 tests) — all greenCloses
Towards #259 (Subdivide and Fill items checked).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes