Repository: cpederkoff/stl-to-voxel

Issue:
Empty output when normal are incorrect
I've noticed that when the normals of my mesh are flipped, the code outputs a series of black images. When using a previous version (0.9.3) it works with these incorrect normals. I thought I would warn you.
I've attached an example of a mesh that works fine on 0.9.3, but not on the latest version (0.10.0)
[example_incorrect_normals.zip](https://github.com/user-attachments/files/18276826/example_incorrect_normals.zip)

code used to replicate the issue: 

`stltovoxel.convert_file(filename , out_file, resolution=253)`

Hints from issue comments:
@Cjdcoy: Indeed, just getting series of black image with latest version whereas 0.9.3 works fine

Solution patch:
diff --git a/stltovoxel/perimeter.py b/stltovoxel/perimeter.py
index 95663b8..5181758 100644
--- a/stltovoxel/perimeter.py
+++ b/stltovoxel/perimeter.py
@@ -69,7 +69,9 @@ def paint_y_axis(lines, pixels, x):
     for target_y, inside_change in target_ys:
         target_y = int(np.ceil(target_y))
         assert target_y >= 0
-        if inside > 0:
+        # If inside > 0, we are inside the polygon
+        # If inside < 0, the normals are likely inverted and we should still paint
+        if inside != 0:
             # Bulk assign all pixels between yi -> target_y
             pixels[yi:target_y, int(x)] = True
 


Test patch:
diff --git a/test/test_perimeter.py b/test/test_perimeter.py
index 4f10cd7..1ea83aa 100644
--- a/test/test_perimeter.py
+++ b/test/test_perimeter.py
@@ -1,32 +1,43 @@
 import numpy as np
 import unittest
-
-from stltovoxel import perimeter
+from stltovoxel.perimeter import lines_to_voxels
 
 
 class TestPerimeter(unittest.TestCase):
-    def test_lines_to_pixels(self):
+    # Expected output for the test polygon
+    EXPECTED_POLYGON = [
+        [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
+        [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
+        [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
+        [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+    ]
+
+    def test_lines_to_voxels(self):
         test = [[(0, 0, 0), (3, 0, 0)],
                 [(3, 0, 0), (9, 9, 0)],
                 [(9, 9, 0), (3, 9, 0)],
                 [(3, 9, 0), (0, 0, 0)]]
         actual = np.zeros((13, 13), dtype=bool)
-        perimeter.lines_to_voxels(test, actual)
-        expected = [
-            [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
-            [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
-            [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
-            [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
-            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
-        self.assertEqual(expected, actual.astype(int).tolist())
+        lines_to_voxels(test, actual)
+        self.assertEqual(self.EXPECTED_POLYGON, actual.astype(int).tolist())
+
+    def test_lines_to_voxels_inverted(self):
+        test = [[(3, 0, 0), (0, 0, 0)],
+                [(9, 9, 0), (3, 0, 0)],
+                [(3, 9, 0), (9, 9, 0)],
+                [(0, 0, 0), (3, 9, 0)]]
+        actual = np.zeros((13, 13), dtype=bool)
+        lines_to_voxels(test, actual)
+        self.assertEqual(self.EXPECTED_POLYGON, actual.astype(int).tolist())
 
 
 if __name__ == '__main__':