|
2 | 2 |
|
3 | 3 | bounds = require('../../utils/bounds') |
4 | 4 |
|
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) -> |
27 | 16 |
|
28 | | - continue if regionAbove.length < 3 |
| 17 | + covered = [] |
29 | 18 |
|
30 | | - boundsAbove = bounds.calculatePathBounds(regionAbove) |
31 | | - continue unless boundsAbove? |
| 19 | + for candidate in regionCandidates |
32 | 20 |
|
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 |
44 | 22 |
|
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? |
48 | 25 |
|
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 |
50 | 36 |
|
51 | | - continue if regionBelow.length < 3 |
| 37 | + candidateWidth = candidateBounds.maxX - candidateBounds.minX |
| 38 | + candidateHeight = candidateBounds.maxY - candidateBounds.minY |
| 39 | + candidateArea = candidateWidth * candidateHeight |
52 | 40 |
|
53 | | - boundsBelow = bounds.calculatePathBounds(regionBelow) |
54 | | - continue unless boundsBelow? |
| 41 | + for ref in regionRefs |
55 | 42 |
|
56 | | - belowWidth = boundsBelow.maxX - boundsBelow.minX |
57 | | - belowHeight = boundsBelow.maxY - boundsBelow.minY |
58 | | - belowArea = belowWidth * belowHeight |
| 43 | + continue if ref.length < 3 |
59 | 44 |
|
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? |
65 | 47 |
|
66 | | - if overlapMinX < overlapMaxX and overlapMinY < overlapMaxY |
| 48 | + refWidth = refBounds.maxX - refBounds.minX |
| 49 | + refHeight = refBounds.maxY - refBounds.minY |
| 50 | + refArea = refWidth * refHeight |
67 | 51 |
|
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) |
71 | 56 |
|
72 | | - if aboveArea > 0 and currentArea > 0 |
| 57 | + if overlapMinX < overlapMaxX and overlapMinY < overlapMaxY |
73 | 58 |
|
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 |
76 | 62 |
|
77 | | - aboveRatio = aboveArea / currentArea |
78 | | - belowRatio = belowArea / currentArea |
| 63 | + if candidateArea > 0 and currentArea > 0 |
79 | 64 |
|
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 |
82 | 67 |
|
83 | | - smallerArea = Math.min(aboveArea, belowArea) |
84 | | - largerArea = Math.max(aboveArea, belowArea) |
85 | | - sizeRatio = smallerArea / largerArea |
| 68 | + candidateRatio = candidateArea / currentArea |
| 69 | + refRatio = refArea / currentArea |
86 | 70 |
|
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 |
90 | 73 |
|
91 | | - fullyCoveredRegions.push(regionAbove) |
92 | | - break |
| 74 | + smallerArea = Math.min(candidateArea, refArea) |
| 75 | + largerArea = Math.max(candidateArea, refArea) |
| 76 | + sizeRatio = smallerArea / largerArea |
93 | 77 |
|
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 |
97 | 81 |
|
98 | | - continue if regionBelow.length < 3 |
| 82 | + covered.push(candidate) |
| 83 | + break |
99 | 84 |
|
100 | | - boundsBelow = bounds.calculatePathBounds(regionBelow) |
101 | | - continue unless boundsBelow? |
| 85 | + return covered |
102 | 86 |
|
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 = |
134 | 88 |
|
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) -> |
138 | 91 |
|
139 | | - if belowArea > 0 and currentArea > 0 |
| 92 | + fullyCoveredRegions = [] |
140 | 93 |
|
141 | | - # Check if overlap is substantial (≥50% of regionBelow). |
142 | | - if (overlapArea / belowArea) >= 0.5 |
| 94 | + currentPathBounds = bounds.calculatePathBounds(currentPath) |
143 | 95 |
|
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 |
146 | 99 |
|
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 |
149 | 103 |
|
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 |
153 | 107 |
|
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...) |
157 | 111 |
|
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...) |
160 | 115 |
|
161 | 116 | return fullyCoveredRegions |
162 | 117 |
|
|
0 commit comments