Skip to content

Commit 8cca9be

Browse files
Copilotjgphilpott
andcommitted
Refactor identifyFullyCoveredRegions into shared helper; remove unused layer52Started
Co-authored-by: jgphilpott <4128208+jgphilpott@users.noreply.github.com>
1 parent 452748b commit 8cca9be

File tree

2 files changed

+80
-127
lines changed

2 files changed

+80
-127
lines changed

src/slicer/skin/exposure/cavity.coffee

Lines changed: 80 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,161 +2,116 @@
22

33
bounds = require('../../utils/bounds')
44

5-
module.exports =
6-
7-
# Identify fully covered regions (have geometry both above AND below).
8-
identifyFullyCoveredRegions: (currentPath, coveringRegionsAbove, coveringRegionsBelow) ->
9-
10-
fullyCoveredRegions = []
11-
12-
currentPathBounds = bounds.calculatePathBounds(currentPath)
13-
14-
return fullyCoveredRegions if not currentPathBounds?
15-
return fullyCoveredRegions if coveringRegionsAbove.length is 0
16-
return fullyCoveredRegions if coveringRegionsBelow.length is 0
17-
18-
currentWidth = currentPathBounds.maxX - currentPathBounds.minX
19-
currentHeight = currentPathBounds.maxY - currentPathBounds.minY
20-
currentArea = currentWidth * currentHeight
21-
22-
# Tolerance used to determine whether a region touches the current path boundary.
23-
# Regions that reach the outer boundary are structural elements, not interior cavities.
24-
BOUNDARY_EPSILON = 0.001
25-
26-
for regionAbove in coveringRegionsAbove
5+
# Scan regionCandidates against regionRefs and return candidates that qualify as
6+
# fully covered interior regions. A candidate qualifies when:
7+
# - It is interior to currentPath (does not touch its boundary).
8+
# - It is the smaller of the two paired regions (candidateArea < refArea).
9+
# - A reference region covers ≥50% of its area.
10+
# - The size ratio between the two regions is within the step-transition range (10-55%).
11+
#
12+
# The boundary check is applied to the candidate only. The reference region may
13+
# legitimately extend to the layer boundary (e.g. the larger slab in an inverted
14+
# pyramid), so it is not subject to the same guard.
15+
findCoveredRegions = (regionCandidates, regionRefs, currentPathBounds, currentArea, BOUNDARY_EPSILON) ->
2716

28-
continue if regionAbove.length < 3
17+
covered = []
2918

30-
boundsAbove = bounds.calculatePathBounds(regionAbove)
31-
continue unless boundsAbove?
19+
for candidate in regionCandidates
3220

33-
# Skip regions that extend to or beyond the current path's boundary.
34-
# Such regions are structural elements (e.g. arch pillars) that continue
35-
# from layer to layer, not interior cavity features that need special skin walls.
36-
# A genuine fully-covered cavity region is entirely inside the current path.
37-
touchesBoundary = (
38-
boundsAbove.minX <= currentPathBounds.minX + BOUNDARY_EPSILON or
39-
boundsAbove.maxX >= currentPathBounds.maxX - BOUNDARY_EPSILON or
40-
boundsAbove.minY <= currentPathBounds.minY + BOUNDARY_EPSILON or
41-
boundsAbove.maxY >= currentPathBounds.maxY - BOUNDARY_EPSILON
42-
)
43-
continue if touchesBoundary
21+
continue if candidate.length < 3
4422

45-
aboveWidth = boundsAbove.maxX - boundsAbove.minX
46-
aboveHeight = boundsAbove.maxY - boundsAbove.minY
47-
aboveArea = aboveWidth * aboveHeight
23+
candidateBounds = bounds.calculatePathBounds(candidate)
24+
continue unless candidateBounds?
4825

49-
for regionBelow in coveringRegionsBelow
26+
# Skip candidates that extend to or beyond the current path's boundary.
27+
# Such regions are structural elements (e.g. arch pillars) that continue
28+
# from layer to layer, not interior cavity features.
29+
touchesBoundary = (
30+
candidateBounds.minX <= currentPathBounds.minX + BOUNDARY_EPSILON or
31+
candidateBounds.maxX >= currentPathBounds.maxX - BOUNDARY_EPSILON or
32+
candidateBounds.minY <= currentPathBounds.minY + BOUNDARY_EPSILON or
33+
candidateBounds.maxY >= currentPathBounds.maxY - BOUNDARY_EPSILON
34+
)
35+
continue if touchesBoundary
5036

51-
continue if regionBelow.length < 3
37+
candidateWidth = candidateBounds.maxX - candidateBounds.minX
38+
candidateHeight = candidateBounds.maxY - candidateBounds.minY
39+
candidateArea = candidateWidth * candidateHeight
5240

53-
boundsBelow = bounds.calculatePathBounds(regionBelow)
54-
continue unless boundsBelow?
41+
for ref in regionRefs
5542

56-
belowWidth = boundsBelow.maxX - boundsBelow.minX
57-
belowHeight = boundsBelow.maxY - boundsBelow.minY
58-
belowArea = belowWidth * belowHeight
43+
continue if ref.length < 3
5944

60-
# Check for overlap between regions.
61-
overlapMinX = Math.max(boundsAbove.minX, boundsBelow.minX)
62-
overlapMaxX = Math.min(boundsAbove.maxX, boundsBelow.maxX)
63-
overlapMinY = Math.max(boundsAbove.minY, boundsBelow.minY)
64-
overlapMaxY = Math.min(boundsAbove.maxY, boundsBelow.maxY)
45+
refBounds = bounds.calculatePathBounds(ref)
46+
continue unless refBounds?
6547

66-
if overlapMinX < overlapMaxX and overlapMinY < overlapMaxY
48+
refWidth = refBounds.maxX - refBounds.minX
49+
refHeight = refBounds.maxY - refBounds.minY
50+
refArea = refWidth * refHeight
6751

68-
overlapWidth = overlapMaxX - overlapMinX
69-
overlapHeight = overlapMaxY - overlapMinY
70-
overlapArea = overlapWidth * overlapHeight
52+
overlapMinX = Math.max(candidateBounds.minX, refBounds.minX)
53+
overlapMaxX = Math.min(candidateBounds.maxX, refBounds.maxX)
54+
overlapMinY = Math.max(candidateBounds.minY, refBounds.minY)
55+
overlapMaxY = Math.min(candidateBounds.maxY, refBounds.maxY)
7156

72-
if aboveArea > 0 and currentArea > 0
57+
if overlapMinX < overlapMaxX and overlapMinY < overlapMaxY
7358

74-
# Check if overlap is substantial (≥50% of regionAbove).
75-
if (overlapArea / aboveArea) >= 0.5
59+
overlapWidth = overlapMaxX - overlapMinX
60+
overlapHeight = overlapMaxY - overlapMinY
61+
overlapArea = overlapWidth * overlapHeight
7662

77-
aboveRatio = aboveArea / currentArea
78-
belowRatio = belowArea / currentArea
63+
if candidateArea > 0 and currentArea > 0
7964

80-
# Check if at least one region is smaller than current layer (step/transition).
81-
if aboveRatio < 0.9 or belowRatio < 0.9
65+
# Check if overlap is substantial (≥50% of candidate).
66+
if (overlapArea / candidateArea) >= 0.5
8267

83-
smallerArea = Math.min(aboveArea, belowArea)
84-
largerArea = Math.max(aboveArea, belowArea)
85-
sizeRatio = smallerArea / largerArea
68+
candidateRatio = candidateArea / currentArea
69+
refRatio = refArea / currentArea
8670

87-
# Filter: size ratio 10-55% (excludes tiny holes and similar-sized regions).
88-
# Only mark as covered when smaller region is from above.
89-
if sizeRatio >= 0.10 and sizeRatio < 0.55 and aboveArea < belowArea
71+
# Check if at least one region is smaller than the current layer.
72+
if candidateRatio < 0.9 or refRatio < 0.9
9073

91-
fullyCoveredRegions.push(regionAbove)
92-
break
74+
smallerArea = Math.min(candidateArea, refArea)
75+
largerArea = Math.max(candidateArea, refArea)
76+
sizeRatio = smallerArea / largerArea
9377

94-
# Second pass: symmetric to first pass but iterates over coveringRegionsBelow.
95-
# Handles inverted case where smaller region is from below (e.g. inverted pyramid).
96-
for regionBelow in coveringRegionsBelow
78+
# Filter: size ratio 10-55% (excludes tiny holes and similar-sized regions).
79+
# Candidate must be the smaller of the two regions.
80+
if sizeRatio >= 0.10 and sizeRatio < 0.55 and candidateArea < refArea
9781

98-
continue if regionBelow.length < 3
82+
covered.push(candidate)
83+
break
9984

100-
boundsBelow = bounds.calculatePathBounds(regionBelow)
101-
continue unless boundsBelow?
85+
return covered
10286

103-
# Skip regions that extend to or beyond the current path's boundary.
104-
touchesBoundary = (
105-
boundsBelow.minX <= currentPathBounds.minX + BOUNDARY_EPSILON or
106-
boundsBelow.maxX >= currentPathBounds.maxX - BOUNDARY_EPSILON or
107-
boundsBelow.minY <= currentPathBounds.minY + BOUNDARY_EPSILON or
108-
boundsBelow.maxY >= currentPathBounds.maxY - BOUNDARY_EPSILON
109-
)
110-
continue if touchesBoundary
111-
112-
belowWidth = boundsBelow.maxX - boundsBelow.minX
113-
belowHeight = boundsBelow.maxY - boundsBelow.minY
114-
belowArea = belowWidth * belowHeight
115-
116-
for regionAbove in coveringRegionsAbove
117-
118-
continue if regionAbove.length < 3
119-
120-
boundsAbove = bounds.calculatePathBounds(regionAbove)
121-
continue unless boundsAbove?
122-
123-
aboveWidth = boundsAbove.maxX - boundsAbove.minX
124-
aboveHeight = boundsAbove.maxY - boundsAbove.minY
125-
aboveArea = aboveWidth * aboveHeight
126-
127-
# Check for overlap between regions.
128-
overlapMinX = Math.max(boundsAbove.minX, boundsBelow.minX)
129-
overlapMaxX = Math.min(boundsAbove.maxX, boundsBelow.maxX)
130-
overlapMinY = Math.max(boundsAbove.minY, boundsBelow.minY)
131-
overlapMaxY = Math.min(boundsAbove.maxY, boundsBelow.maxY)
132-
133-
if overlapMinX < overlapMaxX and overlapMinY < overlapMaxY
87+
module.exports =
13488

135-
overlapWidth = overlapMaxX - overlapMinX
136-
overlapHeight = overlapMaxY - overlapMinY
137-
overlapArea = overlapWidth * overlapHeight
89+
# Identify fully covered regions (have geometry both above AND below).
90+
identifyFullyCoveredRegions: (currentPath, coveringRegionsAbove, coveringRegionsBelow) ->
13891

139-
if belowArea > 0 and currentArea > 0
92+
fullyCoveredRegions = []
14093

141-
# Check if overlap is substantial (≥50% of regionBelow).
142-
if (overlapArea / belowArea) >= 0.5
94+
currentPathBounds = bounds.calculatePathBounds(currentPath)
14395

144-
aboveRatio = aboveArea / currentArea
145-
belowRatio = belowArea / currentArea
96+
return fullyCoveredRegions if not currentPathBounds?
97+
return fullyCoveredRegions if coveringRegionsAbove.length is 0
98+
return fullyCoveredRegions if coveringRegionsBelow.length is 0
14699

147-
# Check if at least one region is smaller than current layer (step/transition).
148-
if aboveRatio < 0.9 or belowRatio < 0.9
100+
currentWidth = currentPathBounds.maxX - currentPathBounds.minX
101+
currentHeight = currentPathBounds.maxY - currentPathBounds.minY
102+
currentArea = currentWidth * currentHeight
149103

150-
smallerArea = Math.min(aboveArea, belowArea)
151-
largerArea = Math.max(aboveArea, belowArea)
152-
sizeRatio = smallerArea / largerArea
104+
# Tolerance used to determine whether a region touches the current path boundary.
105+
# Regions that reach the outer boundary are structural elements, not interior cavities.
106+
BOUNDARY_EPSILON = 0.001
153107

154-
# Filter: size ratio 10-55% (excludes tiny holes and similar-sized regions).
155-
# Only mark as covered when smaller region is from below.
156-
if sizeRatio >= 0.10 and sizeRatio < 0.55 and belowArea < aboveArea
108+
# Pass 1: candidate from above (normal pyramid - smaller region above the transition).
109+
aboveCovered = findCoveredRegions(coveringRegionsAbove, coveringRegionsBelow, currentPathBounds, currentArea, BOUNDARY_EPSILON)
110+
fullyCoveredRegions.push(aboveCovered...)
157111

158-
fullyCoveredRegions.push(regionBelow)
159-
break
112+
# Pass 2: candidate from below (inverted pyramid - smaller region below the transition).
113+
belowCovered = findCoveredRegions(coveringRegionsBelow, coveringRegionsAbove, currentPathBounds, currentArea, BOUNDARY_EPSILON)
114+
fullyCoveredRegions.push(belowCovered...)
160115

161116
return fullyCoveredRegions
162117

src/slicer/skin/exposure/cavity.test.coffee

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,6 @@ describe 'Exposure Detection - Cavity and Hole Detection', ->
985985
# 5x5 slab covers X=[85,135], Y=[85,135].
986986
lines = result.split('\n')
987987
layer51Started = false
988-
layer52Started = false
989988
layer51SkinInfillLines = []
990989
layer51FillLines = []
991990
inFillSection = false
@@ -996,7 +995,6 @@ describe 'Exposure Detection - Cavity and Hole Detection', ->
996995
layer51Started = true
997996
inFillSection = false
998997
else if line.includes('LAYER: 52 of')
999-
layer52Started = true
1000998
break
1001999
else if layer51Started
10021000
if line.includes('TYPE: FILL')

0 commit comments

Comments
 (0)