Skip to content

Commit aa0ac9f

Browse files
Copilotjgphilpott
andcommitted
Fix dome zenith skin infill regression: skip hole paths in findCoveredRegions
Co-authored-by: jgphilpott <4128208+jgphilpott@users.noreply.github.com>
1 parent 8ba8515 commit aa0ac9f

File tree

4 files changed

+121
-4
lines changed

4 files changed

+121
-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:38d56455e61e29acd66db7a425fe4b97dca1d07dde2b29942366ddafec6cfb01
3-
size 775864
2+
oid sha256:232dc72eb279341729adb4a1501ddc06fdf144887da6992b86f697520d6090f3
3+
size 777082
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:f6fd98c88a4ca374a0b30087e7cf5810a729e25941df596424f7b4a544d7dec4
3-
size 766482
2+
oid sha256:a098d7ea5696fbe49d4347e3fa7f95ba685f8f932f1a7978bf5e62320a9c2f04
3+
size 767637

src/slicer/skin/exposure/cavity.coffee

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Cavity detection module for Polyslice.
22

33
bounds = require('../../utils/bounds')
4+
primitives = require('../../utils/primitives')
45

56
# Scan regionCandidates against regionRefs and return candidates that qualify as
67
# fully covered interior regions. A candidate qualifies when:
78
# - 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.
811
# - It is the smaller of the two paired regions (candidateArea < refArea).
912
# - A reference region covers ≥50% of the candidate region's area.
1013
# - The size ratio between the two regions is below the step-transition ceiling (<55%).
@@ -36,6 +39,26 @@ findCoveredRegions = (regionCandidates, regionRefs, currentPathBounds, currentAr
3639
)
3740
continue if touchesBoundary
3841

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).
46+
candidateCenterX = (candidateBounds.minX + candidateBounds.maxX) / 2
47+
candidateCenterY = (candidateBounds.minY + candidateBounds.maxY) / 2
48+
isHolePath = false
49+
50+
for otherPath in regionCandidates
51+
52+
continue if otherPath is candidate
53+
continue if otherPath.length < 3
54+
55+
if primitives.pointInPolygon({ x: candidateCenterX, y: candidateCenterY }, otherPath)
56+
57+
isHolePath = true
58+
break
59+
60+
continue if isHolePath
61+
3962
candidateWidth = candidateBounds.maxX - candidateBounds.minX
4063
candidateHeight = candidateBounds.maxY - candidateBounds.minY
4164
candidateArea = candidateWidth * candidateHeight

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,3 +1253,97 @@ describe 'Exposure Detection - Cavity and Hole Detection', ->
12531253
expect(layer47.skin).toBeGreaterThan(0) # Exposed area around the studs.
12541254
expect(layer47.fillLines).toBeGreaterThan(0) # Regular infill under the studs.
12551255
expect(layer47.fillRegions).toBeGreaterThanOrEqual(6) # One region per stud (6 total).
1256+
1257+
describe 'Dome Zenith Skin Infill (Regression)', ->
1258+
1259+
test 'should generate skin infill on dome zenith exposure patches', ->
1260+
1261+
# Regression test for the dome zenith bug introduced in PR 182.
1262+
# When the 10% minimum size ratio was removed from findCoveredRegions to fix
1263+
# lego stud detection, hole paths from adjacent layers (the small circular opening
1264+
# near the zenith of the dome cavity) were incorrectly classified as "fully covered
1265+
# regions". This caused their corresponding skin infill to be suppressed.
1266+
#
1267+
# The fix: check whether a candidate path is enclosed by another path in the same
1268+
# set (i.e. it is a hole path representing empty space, not a solid feature).
1269+
# Hole paths must not be classified as covered regions.
1270+
#
1271+
# Geometry: box 25x25x12mm with a hemispherical cavity of radius 10mm opening at
1272+
# the build plate. The cavity reaches its zenith at z=10mm (layer 50 of 60).
1273+
# Layers 47-54 are near the zenith and should have skin infill on the small
1274+
# circular exposure patches where the dome ceiling transitions to solid.
1275+
width = 25
1276+
depth = 25
1277+
thickness = 12
1278+
radius = 10
1279+
1280+
boxGeometry = new THREE.BoxGeometry(width, depth, thickness)
1281+
boxMesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial())
1282+
1283+
sphereGeometry = new THREE.SphereGeometry(radius, 64, 48)
1284+
sphereMesh = new THREE.Mesh(sphereGeometry, new THREE.MeshBasicMaterial())
1285+
1286+
# Place sphere center at the bottom face so the upper hemisphere carves a cavity.
1287+
sphereMesh.position.set(0, 0, -(thickness / 2))
1288+
sphereMesh.updateMatrixWorld()
1289+
1290+
# Perform CSG subtraction to create the dome cavity.
1291+
resultMesh = await Polytree.subtract(boxMesh, sphereMesh)
1292+
1293+
# Position final mesh with build plate at Z=0.
1294+
finalMesh = new THREE.Mesh(resultMesh.geometry, resultMesh.material)
1295+
finalMesh.position.set(0, 0, thickness / 2)
1296+
finalMesh.updateMatrixWorld()
1297+
1298+
# Configure slicer with exposure detection enabled.
1299+
slicer.setLayerHeight(0.2)
1300+
slicer.setShellSkinThickness(0.8) # 4 skin layers.
1301+
slicer.setShellWallThickness(0.8)
1302+
slicer.setVerbose(true)
1303+
slicer.setAutohome(false)
1304+
slicer.setExposureDetection(true)
1305+
slicer.setInfillDensity(20)
1306+
1307+
# Slice the mesh.
1308+
result = slicer.slice(finalMesh)
1309+
1310+
# Parse the G-code and find skin infill lines per layer.
1311+
lines = result.split('\n')
1312+
skinInfillByLayer = {}
1313+
currentLayer = null
1314+
1315+
for line in lines
1316+
1317+
layerMatch = line.match(/LAYER:\s*(\d+) of/)
1318+
1319+
if layerMatch
1320+
1321+
currentLayer = parseInt(layerMatch[1])
1322+
1323+
else if currentLayer? and line.includes('Moving to skin infill line')
1324+
1325+
skinInfillByLayer[currentLayer] = (skinInfillByLayer[currentLayer] || 0) + 1
1326+
1327+
# Total layers = 60 (12mm / 0.2mm).
1328+
# The dome zenith is at z=10mm (layer 50).
1329+
# Exposure patches appear at layers near the zenith where the small hole disappears.
1330+
# Before the fix: hole paths from adjacent layers were classified as covered regions,
1331+
# suppressing all skin infill in that area.
1332+
# After the fix: those hole paths are recognised as holes (enclosed by the outer
1333+
# square boundary in the same set) and are no longer classified as covered regions.
1334+
# Layers 49-54 should therefore have skin infill.
1335+
zenithLayerTotal = 0
1336+
1337+
for layerIndex in [49..54]
1338+
1339+
zenithLayerTotal += skinInfillByLayer[layerIndex] || 0
1340+
1341+
# Verify that skin infill is generated at the dome zenith exposure patches.
1342+
expect(zenithLayerTotal).toBeGreaterThan(0)
1343+
1344+
# Also verify lego-stud-style covered region detection still works: the pyramid
1345+
# test in the 'Fully Covered Areas Exclusion' suite is the canonical check, but
1346+
# as a sanity guard verify that the total skin infill count is reasonable
1347+
# (dome should have significantly more skin than a solid box of the same size).
1348+
totalSkinInfill = (result.match(/Moving to skin infill line/g) || []).length
1349+
expect(totalSkinInfill).toBeGreaterThan(50)

0 commit comments

Comments
 (0)