|
| 1 | +# Boolean Operations Debugging Guide |
| 2 | + |
| 3 | +## Quick Start |
| 4 | + |
| 5 | +When you return to debug this implementation, start here: |
| 6 | + |
| 7 | +### 1. Open the Test Page |
| 8 | +```bash |
| 9 | +cd /Users/jonobrandel/Documents/two.js |
| 10 | +open tests/boolean-operations.html |
| 11 | +``` |
| 12 | + |
| 13 | +Open browser console (F12) to see any logs. |
| 14 | + |
| 15 | +### 2. Current Issue |
| 16 | + |
| 17 | +**Problem:** Paths are generated but incomplete - only showing small arc segments instead of full shapes. |
| 18 | + |
| 19 | +**Expected Results:** |
| 20 | +- Union of two circles → Full Venn diagram outline |
| 21 | +- Subtract → Full donut/ring shape |
| 22 | +- Intersect → Full lens shape |
| 23 | +- Exclude → Two full crescent shapes |
| 24 | + |
| 25 | +**Actual Results:** |
| 26 | +- Union → Small arc segment only |
| 27 | +- Subtract → Small arc segment only |
| 28 | +- Intersect → Single line segment only |
| 29 | +- Exclude → Small arc segment only |
| 30 | + |
| 31 | +## Debugging Strategy |
| 32 | + |
| 33 | +### Step 1: Add Logging to `constructBooleanResult()` |
| 34 | + |
| 35 | +Edit `src/utils/boolean-result.js` around line 615: |
| 36 | + |
| 37 | +```javascript |
| 38 | +export function constructBooleanResult(paths, operation, allIntersections) { |
| 39 | + console.log('=== BOOLEAN OPERATION START ==='); |
| 40 | + console.log('Operation:', operation); |
| 41 | + console.log('Number of paths:', paths.length); |
| 42 | + console.log('Number of intersections:', allIntersections ? allIntersections.length : 0); |
| 43 | + |
| 44 | + // ... existing code ... |
| 45 | + |
| 46 | + // After splitting |
| 47 | + console.log('Split paths:', splitPaths.length); |
| 48 | + |
| 49 | + // After creating segments |
| 50 | + console.log('Total segments created:', segments.length); |
| 51 | + console.log('Segments by path:', splitPaths.map((p, i) => ({ |
| 52 | + pathIndex: i, |
| 53 | + vertices: p.vertices.length, |
| 54 | + closed: p.closed |
| 55 | + }))); |
| 56 | + |
| 57 | + // After classification |
| 58 | + const keptSegments = segments.filter(s => s.keep); |
| 59 | + console.log('Segments marked to keep:', keptSegments.length, '/', segments.length); |
| 60 | + console.log('Keep ratio:', (keptSegments.length / segments.length * 100).toFixed(1) + '%'); |
| 61 | + |
| 62 | + // After tracing |
| 63 | + console.log('Contours traced:', contours.length); |
| 64 | + contours.forEach((c, i) => { |
| 65 | + console.log(`Contour ${i}:`, { |
| 66 | + segments: c.segments.length, |
| 67 | + closed: c.closed |
| 68 | + }); |
| 69 | + }); |
| 70 | + |
| 71 | + // After building result |
| 72 | + if (resultPath) { |
| 73 | + console.log('Result path vertices:', resultPath.vertices.length); |
| 74 | + console.log('Result path closed:', resultPath.closed); |
| 75 | + } else { |
| 76 | + console.log('Result path is NULL'); |
| 77 | + } |
| 78 | + console.log('=== BOOLEAN OPERATION END ===\n'); |
| 79 | + |
| 80 | + return resultPath; |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +### Step 2: Rebuild and Test |
| 85 | + |
| 86 | +```bash |
| 87 | +npm run build |
| 88 | +# Refresh browser and check console |
| 89 | +``` |
| 90 | + |
| 91 | +### Step 3: Analyze the Logs |
| 92 | + |
| 93 | +Look for these red flags: |
| 94 | + |
| 95 | +#### Issue: Too Few Segments Kept |
| 96 | +``` |
| 97 | +Segments marked to keep: 2 / 40 |
| 98 | +Keep ratio: 5.0% |
| 99 | +``` |
| 100 | +**Diagnosis:** Segment classification is wrong |
| 101 | +**Fix Location:** `classifySegments()` function (line 338) |
| 102 | +**Possible causes:** |
| 103 | +- `pointInAnyPath()` returning incorrect results |
| 104 | +- Transformation matrix issues |
| 105 | +- Operation rules in `shouldKeepSegment()` are inverted |
| 106 | + |
| 107 | +#### Issue: Contours Have Few Segments |
| 108 | +``` |
| 109 | +Contour 0: { segments: 2, closed: false } |
| 110 | +``` |
| 111 | +**Diagnosis:** Contour tracing stopping too early |
| 112 | +**Fix Location:** `traceContours()` function (line 452) |
| 113 | +**Possible causes:** |
| 114 | +- Adjacency map not built correctly |
| 115 | +- Anchor keys not matching due to precision issues |
| 116 | +- `findNextSegment()` not finding connected segments |
| 117 | + |
| 118 | +#### Issue: Many Segments Kept But Few Vertices |
| 119 | +``` |
| 120 | +Segments marked to keep: 35 / 40 |
| 121 | +Contours traced: 1 |
| 122 | +Contour 0: { segments: 35, closed: true } |
| 123 | +Result path vertices: 3 |
| 124 | +``` |
| 125 | +**Diagnosis:** Path construction failing |
| 126 | +**Fix Location:** `buildResultPath()` function (line 534) |
| 127 | +**Possible causes:** |
| 128 | +- Vertices not being added properly |
| 129 | +- Commands not set correctly |
| 130 | +- Clone operation failing |
| 131 | + |
| 132 | +### Step 4: Targeted Fixes |
| 133 | + |
| 134 | +Based on the diagnosis from Step 3, try these fixes: |
| 135 | + |
| 136 | +#### Fix 1: If Segment Classification is Wrong |
| 137 | + |
| 138 | +Check if `pointInPath()` is working: |
| 139 | + |
| 140 | +```javascript |
| 141 | +// Add to classifySegments() before line 358 |
| 142 | +console.log('Testing segment', i, 'midpoint:', midpoint, 'pathIndex:', pathIndex); |
| 143 | +console.log(' World coords:', worldX, worldY); |
| 144 | +console.log(' Inside other?', isInsideOther); |
| 145 | +console.log(' Keep?', segment.keep); |
| 146 | +``` |
| 147 | + |
| 148 | +Try inverting the logic: |
| 149 | +```javascript |
| 150 | +// In shouldKeepSegment() - line 301 |
| 151 | +case 'union': |
| 152 | + return !isInsideOther; // Try: return isInsideOther; |
| 153 | +``` |
| 154 | + |
| 155 | +#### Fix 2: If Contour Tracing is Wrong |
| 156 | + |
| 157 | +Check adjacency map: |
| 158 | + |
| 159 | +```javascript |
| 160 | +// Add to traceContours() after line 453 |
| 161 | +console.log('Adjacency map size:', adjacencyMap.size); |
| 162 | +console.log('Adjacency map entries:'); |
| 163 | +adjacencyMap.forEach((segments, key) => { |
| 164 | + console.log(` ${key}: ${segments.length} segments`); |
| 165 | +}); |
| 166 | +``` |
| 167 | + |
| 168 | +Check if segments are connecting: |
| 169 | + |
| 170 | +```javascript |
| 171 | +// Add inside the tracing loop (around line 505) |
| 172 | +console.log('Current segment end:', currentEnd); |
| 173 | +console.log('Looking for next segment...'); |
| 174 | +const nextSegment = findNextSegment(adjacencyMap, currentEnd, usedSegments); |
| 175 | +console.log('Found next segment:', !!nextSegment); |
| 176 | +``` |
| 177 | + |
| 178 | +#### Fix 3: If Path Construction is Wrong |
| 179 | + |
| 180 | +Check vertex addition: |
| 181 | + |
| 182 | +```javascript |
| 183 | +// Add to buildResultPath() inside loop (around line 564) |
| 184 | +console.log('Adding segment', s, 'end anchor:', segment.endAnchor); |
| 185 | +console.log(' Command:', endAnchor.command); |
| 186 | +console.log(' Position:', endAnchor.x, endAnchor.y); |
| 187 | +allVertices.push(endAnchor); |
| 188 | +console.log(' Total vertices now:', allVertices.length); |
| 189 | +``` |
| 190 | + |
| 191 | +### Step 5: Test with Simple Case |
| 192 | + |
| 193 | +Create a minimal test with two rectangles (easier to debug than circles): |
| 194 | + |
| 195 | +```javascript |
| 196 | +// Add to boolean-operations.html |
| 197 | +function testSimpleRectangles() { |
| 198 | + const two = new Two({ width: 350, height: 250 }) |
| 199 | + .appendTo(document.getElementById('test-simple')); |
| 200 | + |
| 201 | + const rect1 = two.makeRectangle(120, 125, 80, 80); |
| 202 | + rect1.fill = 'rgba(255, 0, 0, 0.3)'; |
| 203 | + rect1.stroke = '#ccc'; |
| 204 | + |
| 205 | + const rect2 = two.makeRectangle(180, 125, 80, 80); |
| 206 | + rect2.fill = 'rgba(0, 0, 255, 0.3)'; |
| 207 | + rect2.stroke = '#ccc'; |
| 208 | + |
| 209 | + const boolGroup = two.makeBooleanGroup([rect1, rect2], 'union'); |
| 210 | + const result = boolGroup.getResultPath(); |
| 211 | + |
| 212 | + console.log('SIMPLE TEST - Result:', result); |
| 213 | + |
| 214 | + if (result) { |
| 215 | + result.stroke = '#00aa00'; |
| 216 | + result.linewidth = 3; |
| 217 | + two.add(result); |
| 218 | + } |
| 219 | + |
| 220 | + two.update(); |
| 221 | +} |
| 222 | +``` |
| 223 | + |
| 224 | +Rectangles have: |
| 225 | +- Only 4 vertices each |
| 226 | +- Only straight lines (no curves) |
| 227 | +- Easier to visualize and debug |
| 228 | + |
| 229 | +### Step 6: Visual Debugging |
| 230 | + |
| 231 | +Add visual markers to see what's happening: |
| 232 | + |
| 233 | +```javascript |
| 234 | +// After classifySegments() in constructBooleanResult() |
| 235 | +// Draw kept segment midpoints |
| 236 | +keptSegments.forEach(seg => { |
| 237 | + const circle = two.makeCircle(seg.midpoint.x, seg.midpoint.y, 3); |
| 238 | + circle.fill = '#00ff00'; |
| 239 | + circle.noStroke(); |
| 240 | +}); |
| 241 | + |
| 242 | +// Draw discarded segment midpoints |
| 243 | +segments.filter(s => !s.keep).forEach(seg => { |
| 244 | + const circle = two.makeCircle(seg.midpoint.x, seg.midpoint.y, 3); |
| 245 | + circle.fill = '#ff0000'; |
| 246 | + circle.noStroke(); |
| 247 | +}); |
| 248 | +``` |
| 249 | + |
| 250 | +This will show green dots for kept segments and red dots for discarded segments. |
| 251 | + |
| 252 | +## Common Issues and Solutions |
| 253 | + |
| 254 | +### Issue: "Cannot read properties of undefined" |
| 255 | +**Cause:** Array bounds issues or null checks missing |
| 256 | +**Fix:** Add more defensive checks in `insertAnchorAtIntersection()` |
| 257 | + |
| 258 | +### Issue: Segments not connecting |
| 259 | +**Cause:** Anchor key precision mismatch |
| 260 | +**Fix:** Adjust `anchorKey()` precision (try 2 or 4 decimal places instead of 3) |
| 261 | + |
| 262 | +### Issue: Wrong segments kept for operation |
| 263 | +**Cause:** Logic inverted or transformation issues |
| 264 | +**Fix:** Double-check `shouldKeepSegment()` rules match the operation semantics |
| 265 | + |
| 266 | +### Issue: Empty result path |
| 267 | +**Cause:** All segments discarded or contour tracing failed |
| 268 | +**Fix:** Check that at least some segments are marked `keep: true` |
| 269 | + |
| 270 | +## Quick Reference: Operation Rules |
| 271 | + |
| 272 | +For a segment on path A being tested against path B: |
| 273 | + |
| 274 | +| Operation | Segment Inside B | Keep Segment? | |
| 275 | +|-----------|------------------|---------------| |
| 276 | +| Union | No | ✓ Yes | |
| 277 | +| Union | Yes | ✗ No | |
| 278 | +| Subtract | No (A only) | ✓ Yes | |
| 279 | +| Subtract | Yes (A only) | ✗ No | |
| 280 | +| Subtract | Any (B segments) | ✗ No | |
| 281 | +| Intersect | Yes | ✓ Yes | |
| 282 | +| Intersect | No | ✗ No | |
| 283 | +| Exclude | No | ✓ Yes | |
| 284 | +| Exclude | Yes | ✗ No | |
| 285 | + |
| 286 | +## Files to Check |
| 287 | + |
| 288 | +Main implementation: |
| 289 | +- `src/utils/boolean-result.js` - All the logic |
| 290 | +- `src/boolean-group.js` - Entry point |
| 291 | +- `tests/boolean-operations.html` - Test page |
| 292 | + |
| 293 | +Supporting files: |
| 294 | +- `src/utils/boolean-operations.js` - Intersection detection (working) |
| 295 | +- `src/utils/hit-test.js` - Ray casting (used for point-in-path) |
| 296 | + |
| 297 | +## Rebuild Commands |
| 298 | + |
| 299 | +```bash |
| 300 | +# Full rebuild |
| 301 | +npm run build && npm run lint |
| 302 | + |
| 303 | +# Quick rebuild (skip lint) |
| 304 | +npm run build |
| 305 | + |
| 306 | +# Watch mode (if available) |
| 307 | +npm run dev |
| 308 | +``` |
| 309 | + |
| 310 | +## Success Criteria |
| 311 | + |
| 312 | +When fixed, you should see: |
| 313 | +- Union: Full outline combining both shapes |
| 314 | +- Subtract: Full shape with hole(s) removed |
| 315 | +- Intersect: Full overlapping region |
| 316 | +- Exclude: Full non-overlapping regions |
| 317 | + |
| 318 | +Each should have 20-50+ vertices for smooth curves. |
0 commit comments