@@ -6,8 +6,9 @@ primitives = require('../../utils/primitives')
66# Scan regionCandidates against regionRefs and return candidates that qualify as
77# fully covered interior regions. A candidate qualifies when:
88# - It is interior to currentPath (does not touch its boundary).
9- # - It is NOT a hole path (i.e. not enclosed by another path in the same layer set).
10- # Hole paths represent empty space in an adjacent layer, not solid features.
9+ # - It is a solid feature at its nesting level, NOT a hole path. Nesting parity is
10+ # determined by counting how many other paths in regionCandidates contain the
11+ # candidate's centre (even = solid structure, odd = hole/cavity).
1112# - It is the smaller of the two paired regions (candidateArea < refArea).
1213# - A reference region covers ≥50% of the candidate region's area.
1314# - The size ratio between the two regions is below the step-transition ceiling (<55%).
@@ -39,25 +40,42 @@ findCoveredRegions = (regionCandidates, regionRefs, currentPathBounds, currentAr
3940 )
4041 continue if touchesBoundary
4142
42- # Skip candidates that are hole paths (enclosed by another path in the same set).
43- # Hole paths represent empty space (cavities/openings) in the adjacent layer, not
44- # solid features. Treating them as covered regions would suppress skin infill
45- # on the corresponding exposure patches in the current layer (e.g. dome zenith).
43+ # Determine whether this candidate is a hole path using the even-odd nesting rule.
44+ # Count how many other paths in regionCandidates contain the candidate's centre point.
45+ # Odd containment count → the candidate is inside an odd number of paths → it is
46+ # empty space (a hole or cavity) in the adjacent layer, not a solid feature.
47+ # Even containment count (0 included) → it is a solid structure at that nesting level.
48+ #
49+ # This handles arbitrarily deep nesting (structure→hole→structure→…) correctly:
50+ # - outer structure: 0 containing paths (even) → structure ✓
51+ # - hole inside that structure: 1 containing path (odd) → hole ✓
52+ # - structure inside the hole: 2 containing paths (even) → structure ✓
53+ #
54+ # Bounding-box containment is checked first as a cheap pre-filter so that the more
55+ # expensive pointInPolygon call is only made for paths whose bounds actually enclose
56+ # the candidate centre.
4657 candidateCenterX = (candidateBounds .minX + candidateBounds .maxX ) / 2
4758 candidateCenterY = (candidateBounds .minY + candidateBounds .maxY ) / 2
48- isHolePath = false
59+ nestingCount = 0
4960
5061 for otherPath in regionCandidates
5162
5263 continue if otherPath is candidate
5364 continue if otherPath .length < 3
5465
66+ otherBounds = bounds .calculatePathBounds (otherPath)
67+ continue unless otherBounds?
68+
69+ # Cheap bounding-box pre-filter.
70+ continue if candidateCenterX < otherBounds .minX or candidateCenterX > otherBounds .maxX
71+ continue if candidateCenterY < otherBounds .minY or candidateCenterY > otherBounds .maxY
72+
5573 if primitives .pointInPolygon ({ x : candidateCenterX, y : candidateCenterY }, otherPath)
5674
57- isHolePath = true
58- break
75+ nestingCount += 1
5976
60- continue if isHolePath
77+ # Odd nesting count → hole/cavity path; skip it.
78+ continue if nestingCount % 2 is 1
6179
6280 candidateWidth = candidateBounds .maxX - candidateBounds .minX
6381 candidateHeight = candidateBounds .maxY - candidateBounds .minY
0 commit comments