Skip to content

Commit bc33cfb

Browse files
committed
Use derivative tiebreaker
1 parent 5dedb08 commit bc33cfb

File tree

4 files changed

+598
-1
lines changed

4 files changed

+598
-1
lines changed

node-graph/gcore/src/subpath/structs.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ impl BezierHandles {
228228
_ => self,
229229
}
230230
}
231+
232+
/// Returns the tangent vector at the start of the curve (t=0)
233+
pub fn derivative_at_start(&self, start: DVec2, end: DVec2) -> DVec2 {
234+
match self {
235+
BezierHandles::Linear => end - start,
236+
BezierHandles::Quadratic { handle } => 2.0 * (handle - start),
237+
BezierHandles::Cubic { handle_start, .. } => 3.0 * (handle_start - start),
238+
}
239+
}
240+
241+
/// Returns the tangent vector at the end of the curve (t=1)
242+
pub fn derivative_at_end(&self, start: DVec2, end: DVec2) -> DVec2 {
243+
match self {
244+
BezierHandles::Linear => end - start,
245+
BezierHandles::Quadratic { handle } => 2.0 * (end - handle),
246+
BezierHandles::Cubic { handle_end, .. } => 3.0 * (end - handle_end),
247+
}
248+
}
231249
}
232250

233251
/// Representation of a bezier curve with 2D points.
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Vector Mesh Fill Rendering - Implementation Plan
2+
3+
## Overview
4+
This document outlines the improvements needed for the vector mesh fill rendering system based on code review feedback for PR #3388. All changes will be made to `node-graph/gcore/src/vector/vector_attributes.rs`.
5+
6+
---
7+
8+
## Issues and Solutions
9+
10+
### 1. CRITICAL: Multiple Edges Between Same Vertices
11+
12+
**Problem:**
13+
When multiple edges connect the same two vertices (e.g., two curves with different curvatures), they produce the same angle during neighbor sorting. This causes incorrect edge ordering when finding minimal faces.
14+
15+
**Solution:**
16+
Add a derivative-based tiebreaker to distinguish edges with the same angle.
17+
18+
**Implementation Steps:**
19+
20+
1. **Add `derivative_at_start()` method to `BezierHandles` in `node-graph/gcore/src/subpath/structs.rs`:**
21+
```rust
22+
/// Returns the tangent vector at the start of the curve (t=0)
23+
pub fn derivative_at_start(&self, start: DVec2, end: DVec2) -> DVec2 {
24+
match self {
25+
BezierHandles::Linear => end - start,
26+
BezierHandles::Quadratic { handle } => 2.0 * (handle - start),
27+
BezierHandles::Cubic { handle_start, .. } => 3.0 * (handle_start - start),
28+
}
29+
}
30+
```
31+
32+
2. **Add `derivative_at_end()` method to `BezierHandles` in `node-graph/gcore/src/subpath/structs.rs`:**
33+
```rust
34+
/// Returns the tangent vector at the end of the curve (t=1)
35+
pub fn derivative_at_end(&self, start: DVec2, end: DVec2) -> DVec2 {
36+
match self {
37+
BezierHandles::Linear => end - start,
38+
BezierHandles::Quadratic { handle } => 2.0 * (end - handle),
39+
BezierHandles::Cubic { handle_end, .. } => 3.0 * (end - handle_end),
40+
}
41+
}
42+
```
43+
44+
3. **Update sorting in `find_closed_regions()` (around line 1052-1059):**
45+
- After comparing angles, if they're equal (within epsilon), compare derivative angles
46+
- For edges leaving a point, use `derivative_at_start()`
47+
- Account for reversed edges by using the appropriate derivative direction
48+
49+
**Code Location:** Lines 1052-1059 in `vector_attributes.rs`
50+
51+
**Priority:** CRITICAL (may cause rendering bugs with multiple edges)
52+
53+
---
54+
55+
### 2. PERFORMANCE: Unnecessary Edge Calculation
56+
57+
**Problem:**
58+
In `find_minimal_face_from_edge()`, the code recalculates angles and searches through all neighbors to find the next edge (lines 1115-1123), even though neighbors are already sorted by angle in `find_closed_regions()`.
59+
60+
**Current Approach:**
61+
```rust
62+
let next = neighbors.iter()
63+
.filter(|(seg, _next, _rev)| *seg != prev_segment)
64+
.min_by(|a, b| {
65+
let dir_a = ...;
66+
let dir_b = ...;
67+
let angle_a = angle_between(prev_direction, dir_a);
68+
let angle_b = angle_between(prev_direction, dir_b);
69+
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
70+
})?;
71+
```
72+
73+
**Better Approach:**
74+
```rust
75+
// Find the index of the edge we came from
76+
let prev_index = neighbors.iter()
77+
.position(|(seg, _next, _rev)| *seg == prev_segment)?;
78+
79+
// The next edge in the sorted list is the minimal face edge
80+
// Wrap around using modulo to handle circular list
81+
let next = neighbors[(prev_index + 1) % neighbors.len()];
82+
```
83+
84+
**Code Location:** Lines 1115-1123 in `vector_attributes.rs`
85+
86+
**Benefits:**
87+
- O(n) → O(1) lookup per vertex
88+
- Eliminates angle calculations in the hot loop
89+
- Simpler, more readable code
90+
91+
**Priority:** NICE TO HAVE (performance optimization)
92+
93+
---
94+
95+
### 3. FEATURE: Outer Face Culling
96+
97+
**Problem:**
98+
The current implementation includes the outer boundary face (e.g., the outer square of a grid mesh), which should often be excluded.
99+
100+
**Solution:**
101+
Use the shoelace formula to compute signed area and filter out counterclockwise (outer) faces.
102+
103+
**Implementation Steps:**
104+
105+
1. **Add `compute_signed_area()` helper function:**
106+
```rust
107+
fn compute_signed_area(&self, face: &FoundSubpath) -> f64 {
108+
let mut area = 0.0;
109+
for edge in &face.edges {
110+
let start_pos = self.point_domain.positions()[edge.start];
111+
let end_pos = self.point_domain.positions()[edge.end];
112+
area += (end_pos.x - start_pos.x) * (end_pos.y + start_pos.y);
113+
}
114+
area * 0.5
115+
}
116+
```
117+
118+
2. **Filter faces in `find_closed_regions()` before returning:**
119+
```rust
120+
regions.retain(|face| {
121+
// Always include 2-edge faces (floating point issues with area)
122+
if face.edges.len() == 2 {
123+
return true;
124+
}
125+
126+
let area = self.compute_signed_area(face);
127+
// Keep clockwise faces (negative area) and exclude counterclockwise (positive area)
128+
area < 0.0
129+
});
130+
```
131+
132+
**Code Location:** End of `find_closed_regions()` (around line 1078)
133+
134+
**Additional Considerations:**
135+
- Floating point precision: use a small epsilon if needed
136+
- Consider making this behavior optional via a parameter
137+
- Document the winding order convention
138+
139+
**Priority:** NICE TO HAVE (feature enhancement)
140+
141+
---
142+
143+
### 4. CODE QUALITY: Loop Style
144+
145+
**Problem:**
146+
The loop uses an awkward pattern with manual iteration counting and break condition.
147+
148+
**Current Code (lines 1097-1104):**
149+
```rust
150+
let mut iteration = 0;
151+
let max_iterations = adjacency.len() * 2;
152+
153+
loop {
154+
iteration += 1;
155+
156+
if iteration > max_iterations {
157+
return None;
158+
}
159+
// ...
160+
}
161+
```
162+
163+
**Better Code:**
164+
```rust
165+
let max_iterations = adjacency.len() * 2;
166+
167+
for _iteration in 1..=max_iterations {
168+
// ... loop body ...
169+
}
170+
171+
// If we exit the loop without returning, the path didn't close
172+
None
173+
```
174+
175+
**Code Location:** Lines 1097-1104 in `vector_attributes.rs`
176+
177+
**Benefits:**
178+
- More idiomatic Rust
179+
- Clearer intent
180+
- Automatic bounds checking
181+
182+
**Priority:** NITPICK (code style)
183+
184+
---
185+
186+
### 5. CODE QUALITY: Redundant Check
187+
188+
**Problem:**
189+
The infinite loop prevention check at line 1145 is redundant with the max_iterations check.
190+
191+
**Current Code (line 1145):**
192+
```rust
193+
// Prevent infinite loops
194+
if path.len() > adjacency.len() {
195+
return None;
196+
}
197+
```
198+
199+
**Action:**
200+
Remove this check entirely. The `for` loop (after implementing #4) or the `max_iterations` check already prevents infinite loops.
201+
202+
**Code Location:** Line 1145 in `vector_attributes.rs`
203+
204+
**Priority:** NITPICK (code cleanup)
205+
206+
---
207+
208+
### 6. CODE QUALITY: Cycle Detection Condition
209+
210+
**Problem:**
211+
The cycle detection at line 1138 includes `e.id != next_segment` which may be unnecessary.
212+
213+
**Current Code (line 1138):**
214+
```rust
215+
if path.iter().any(|e| e.end == next_point && e.id != next_segment) {
216+
return None;
217+
}
218+
```
219+
220+
**Investigation Needed:**
221+
- Is it possible for `e.end == next_point` and `e.id == next_segment`?
222+
- If the first condition is sufficient, simplify to:
223+
```rust
224+
if path.iter().any(|e| e.end == next_point) {
225+
return None;
226+
}
227+
```
228+
229+
**Code Location:** Line 1138 in `vector_attributes.rs`
230+
231+
**Priority:** NITPICK (code clarity)
232+
233+
---
234+
235+
### 7. CODE QUALITY: Code Organization
236+
237+
**Problem:**
238+
The `if next_point == target` check happens before pushing to path, then duplicates the path-push logic inside.
239+
240+
**Current Code (lines 1125-1134):**
241+
```rust
242+
if next_point == target {
243+
// Completed the cycle
244+
path.push(HalfEdge::new(next_segment, current, next_point, next_reversed));
245+
246+
// Mark all half-edges as used
247+
for edge in &path {
248+
used_half_edges.insert((edge.id, edge.reverse));
249+
}
250+
251+
return Some(FoundSubpath::new(path));
252+
}
253+
```
254+
255+
**Better Organization:**
256+
Move this check to after line 1144 (after the cycle detection) and reuse the path-push logic:
257+
258+
```rust
259+
// Check if we've created a cycle (might not be back to start)
260+
if path.iter().any(|e| e.end == next_point && e.id != next_segment) {
261+
return None;
262+
}
263+
264+
path.push(HalfEdge::new(next_segment, current, next_point, next_reversed));
265+
266+
// Check if we've completed the face
267+
if next_point == target {
268+
// Mark all half-edges as used
269+
for edge in &path {
270+
used_half_edges.insert((edge.id, edge.reverse));
271+
}
272+
return Some(FoundSubpath::new(path));
273+
}
274+
275+
prev_segment = next_segment;
276+
current = next_point;
277+
```
278+
279+
**Code Location:** Lines 1125-1147 in `vector_attributes.rs`
280+
281+
**Benefits:**
282+
- Eliminates code duplication
283+
- More logical flow
284+
- Easier to maintain
285+
286+
**Priority:** NITPICK (code organization)
287+
288+
---
289+
290+
## Implementation Order
291+
292+
### Phase 1: Critical Fixes
293+
1. **Multiple Edges Handling** (#1)
294+
- Add derivative methods to BezierHandles
295+
- Update sorting logic with tiebreaker
296+
297+
### Phase 2: Performance & Features
298+
2. **Performance Optimization** (#2)
299+
- Replace min_by with index-based lookup
300+
3. **Outer Face Culling** (#3)
301+
- Add signed area calculation
302+
- Filter counterclockwise faces
303+
304+
### Phase 3: Code Quality
305+
4. **Loop Style** (#4)
306+
- Convert to for loop
307+
5. **Remove Redundant Check** (#5)
308+
- Delete redundant infinite loop check
309+
6. **Cycle Detection** (#6)
310+
- Investigate and simplify if possible
311+
7. **Code Organization** (#7)
312+
- Reorganize target-check logic
313+
314+
---
315+
316+
## Testing Considerations
317+
318+
After implementing these changes, test with:
319+
320+
1. **Multiple edges test:** Create a mesh with multiple curves between the same vertices
321+
2. **Grid mesh test:** Verify outer face is properly culled
322+
3. **Complex mesh test:** Test with various mesh topologies
323+
4. **Performance test:** Measure improvement from optimization #2
324+
5. **Edge cases:**
325+
- 2-edge faces
326+
- Self-intersecting paths
327+
- Degenerate curves
328+
329+
---
330+
331+
## Notes
332+
333+
- All changes maintain backward compatibility
334+
- No API changes required (internal implementation only)
335+
- Changes are localized to vector_attributes.rs and subpath/structs.rs
336+
- Derivative methods in BezierHandles may be useful for other features in the future
337+
338+
---
339+
340+
## Reviewer Approval Status
341+
342+
**Status:** ✅ Approved with suggestions
343+
344+
**Critical Items:** Issue #1 (if reviewer's understanding is correct)
345+
**Nice to Have:** Issues #2, #3
346+
**Nitpicks:** Issues #4-7

0 commit comments

Comments
 (0)