Skip to content

Commit 3e39c94

Browse files
Copilotjgphilpott
andcommitted
Fix inverted pyramid fully covered region detection in cavity.coffee
Co-authored-by: jgphilpott <4128208+jgphilpott@users.noreply.github.com>
1 parent c0bbf13 commit 3e39c94

File tree

4 files changed

+186
-4
lines changed

4 files changed

+186
-4
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:24d0eba4f3731cd391d9c8e35904c89dce0e5f502ffea6c04e8e2abf681fbafc
3-
size 521461
2+
oid sha256:25928d39af107a6bf9f515470316a4771e8ee04d0aad2e06b5996a92f71f0748
3+
size 588366
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:e980eff0f6af5ad0747e7b7639fa526ecd13c544cb01ff3cdd36c3623aa3b7d0
3-
size 680729
2+
oid sha256:da78b3d04de3471a8994eaf01d6f62c2fbccd43045d2708eea3cabdf949ff647
3+
size 752102

src/slicer/skin/exposure/cavity.coffee

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,73 @@ module.exports =
9191
fullyCoveredRegions.push(regionAbove)
9292
break
9393

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
97+
98+
continue if regionBelow.length < 3
99+
100+
boundsBelow = bounds.calculatePathBounds(regionBelow)
101+
continue unless boundsBelow?
102+
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
134+
135+
overlapWidth = overlapMaxX - overlapMinX
136+
overlapHeight = overlapMaxY - overlapMinY
137+
overlapArea = overlapWidth * overlapHeight
138+
139+
if belowArea > 0 and currentArea > 0
140+
141+
# Check if overlap is substantial (≥50% of regionBelow).
142+
if (overlapArea / belowArea) >= 0.5
143+
144+
aboveRatio = aboveArea / currentArea
145+
belowRatio = belowArea / currentArea
146+
147+
# Check if at least one region is smaller than current layer (step/transition).
148+
if aboveRatio < 0.9 or belowRatio < 0.9
149+
150+
smallerArea = Math.min(aboveArea, belowArea)
151+
largerArea = Math.max(aboveArea, belowArea)
152+
sizeRatio = smallerArea / largerArea
153+
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
157+
158+
fullyCoveredRegions.push(regionBelow)
159+
break
160+
94161
return fullyCoveredRegions
95162

96163
# Filter fully covered regions for skin infill exclusion.

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

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,3 +935,118 @@ describe 'Exposure Detection - Cavity and Hole Detection', ->
935935
# 2. Each covered region
936936
# Expect multiple skin sections.
937937
expect(skinSectionCount).toBeGreaterThan(3)
938+
939+
test 'should detect fully covered region when smaller region is from BELOW (inverted pyramid)', ->
940+
941+
# Regression test for inverted pyramid case.
942+
# When the shape is inverted (small base, large top), the fully covered region
943+
# comes from below the transition layer, not above.
944+
# On transition layers, the center area (covered by the small slab below) should
945+
# be detected as "fully covered" and get a skin wall + regular infill,
946+
# while the outer ring should get O-shaped skin infill only.
947+
cubeSize = 10
948+
949+
# Inverted: small base slab (3x3) at bottom, large top slab (5x5) on top.
950+
baseSlab = new THREE.BoxGeometry(3 * cubeSize, 3 * cubeSize, cubeSize)
951+
baseSlabMesh = new THREE.Mesh(baseSlab, new THREE.MeshBasicMaterial())
952+
baseSlabMesh.position.set(0, 0, 0)
953+
baseSlabMesh.updateMatrixWorld()
954+
955+
topSlab = new THREE.BoxGeometry(5 * cubeSize, 5 * cubeSize, cubeSize)
956+
topSlabMesh = new THREE.Mesh(topSlab, new THREE.MeshBasicMaterial())
957+
topSlabMesh.position.set(0, 0, cubeSize)
958+
topSlabMesh.updateMatrixWorld()
959+
960+
invertedMesh = await Polytree.unite(baseSlabMesh, topSlabMesh)
961+
962+
finalMesh = new THREE.Mesh(invertedMesh.geometry, invertedMesh.material)
963+
finalMesh.position.set(0, 0, 0)
964+
finalMesh.updateMatrixWorld()
965+
966+
# Configure slicer with exposure detection enabled.
967+
slicer.setLayerHeight(0.2)
968+
slicer.setShellSkinThickness(0.8) # 4 skin layers.
969+
slicer.setShellWallThickness(0.8)
970+
slicer.setInfillDensity(20)
971+
slicer.setInfillPattern('grid')
972+
slicer.setVerbose(true)
973+
slicer.setAutohome(false)
974+
slicer.setExposureDetection(true)
975+
976+
result = slicer.slice(finalMesh)
977+
978+
# The base slab is 10mm tall = 50 layers (1-50).
979+
# The top slab is 10mm tall = 50 layers (51-100).
980+
# First 4 layers of top slab (51-54) should have adaptive skin.
981+
# Center 3x3 area (covered by small slab below) should NOT have skin infill.
982+
# Outer ring (exposed area) should have skin infill.
983+
# Build plate center = 110x110 (220x220 bed).
984+
# 3x3 slab covers X=[95,125], Y=[95,125].
985+
# 5x5 slab covers X=[85,135], Y=[85,135].
986+
lines = result.split('\n')
987+
layer51Started = false
988+
layer52Started = false
989+
layer51SkinInfillLines = []
990+
layer51FillLines = []
991+
inFillSection = false
992+
993+
for line in lines
994+
995+
if line.includes('LAYER: 51 of')
996+
layer51Started = true
997+
inFillSection = false
998+
else if line.includes('LAYER: 52 of')
999+
layer52Started = true
1000+
break
1001+
else if layer51Started
1002+
if line.includes('TYPE: FILL')
1003+
inFillSection = true
1004+
else if line.includes('TYPE:')
1005+
inFillSection = false
1006+
1007+
if line.includes('Moving to skin infill line')
1008+
layer51SkinInfillLines.push(line)
1009+
else if inFillSection and line.includes('G1') and line.includes(' E')
1010+
layer51FillLines.push(line)
1011+
1012+
# Verify that layer 51 has skin infill (outer ring exposed area).
1013+
expect(layer51SkinInfillLines.length).toBeGreaterThan(0)
1014+
1015+
# Verify NO skin infill lines are in the fully covered center area (95-125).
1016+
# The 3x3 slab below covers X=[95,125] and Y=[95,125].
1017+
# Skin infill should only be in the exposed outer ring, not the center.
1018+
centerSkinInfillCount = 0
1019+
1020+
for line in layer51SkinInfillLines
1021+
1022+
xMatch = line.match(/X([\d.]+)/)
1023+
yMatch = line.match(/Y([\d.]+)/)
1024+
1025+
if xMatch and yMatch
1026+
1027+
xCoord = parseFloat(xMatch[1])
1028+
yCoord = parseFloat(yMatch[1])
1029+
1030+
if xCoord > 95 and xCoord < 125 and yCoord > 95 and yCoord < 125
1031+
centerSkinInfillCount += 1
1032+
1033+
expect(centerSkinInfillCount).toBe(0)
1034+
1035+
# Verify that regular infill IS generated in the covered center area.
1036+
# This ensures structural support in fully covered regions.
1037+
centerFillCount = 0
1038+
1039+
for line in layer51FillLines
1040+
1041+
xMatch = line.match(/X([\d.]+)/)
1042+
yMatch = line.match(/Y([\d.]+)/)
1043+
1044+
if xMatch and yMatch
1045+
1046+
xCoord = parseFloat(xMatch[1])
1047+
yCoord = parseFloat(yMatch[1])
1048+
1049+
if xCoord > 95 and xCoord < 125 and yCoord > 95 and yCoord < 125
1050+
centerFillCount += 1
1051+
1052+
expect(centerFillCount).toBeGreaterThan(0)

0 commit comments

Comments
 (0)