From 8f9a74c44852a2ba56d12973374208ac280f3541 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 30 Aug 2023 10:45:46 -0700 Subject: [PATCH 01/10] Moved all boolean calls to Manifold. --- src/scenic/core/regions.py | 2 +- src/scenic/core/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index e2e0dfd3b..42d668767 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -781,7 +781,7 @@ def __init__( tolerance=1e-6, centerMesh=True, onDirection=None, - engine="scad", + engine="manifold", name=None, additionalDeps=[], ): diff --git a/src/scenic/core/utils.py b/src/scenic/core/utils.py index 7b996e7f6..3f9bfbc69 100644 --- a/src/scenic/core/utils.py +++ b/src/scenic/core/utils.py @@ -173,7 +173,7 @@ def unifyMesh(mesh, verbose=False): return mesh try: - unified_mesh = trimesh.boolean.union(mesh_bodies, engine="scad") + unified_mesh = trimesh.boolean.union(mesh_bodies) except CalledProcessError: # Something went wrong, return the original mesh if verbose: From 2f6fcd1b911878f3a129e51a30c6d2b19312c5b6 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 9 Nov 2023 13:10:46 -0800 Subject: [PATCH 02/10] Added manifold as dependency and tore out engine option. --- pyproject.toml | 3 ++- src/scenic/core/regions.py | 44 +++++--------------------------------- tests/core/test_regions.py | 31 +++++++++++---------------- 3 files changed, 19 insertions(+), 59 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35342a7be..08935288c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "dotmap ~= 1.3", "mapbox_earcut >= 0.12.10", "matplotlib ~= 3.2", + "manifold3d ~= 2.2", "networkx >= 2.6", "numpy ~= 1.24", "opencv-python ~= 4.5", @@ -45,7 +46,7 @@ dependencies = [ "rv-ltl ~= 0.1", "scipy ~= 1.7", "shapely ~= 2.0", - "trimesh >=3.22.5, <4", + "trimesh >=4.0.3, <5", ] [project.optional-dependencies] diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 42d668767..5f8f80e85 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -767,7 +767,6 @@ class MeshRegion(Region): tolerance: Tolerance for internal computations. centerMesh: Whether or not to center the mesh after copying and before transformations. onDirection: The direction to use if an object being placed on this region doesn't specify one. - engine: Which engine to use for mesh operations. Either "blender" or "scad". additionalDeps: Any additional sampling dependencies this region relies on. """ @@ -781,7 +780,6 @@ def __init__( tolerance=1e-6, centerMesh=True, onDirection=None, - engine="manifold", name=None, additionalDeps=[], ): @@ -794,7 +792,6 @@ def __init__( self.tolerance = tolerance self.centerMesh = centerMesh self.onDirection = onDirection - self.engine = engine # Initialize superclass with samplables super().__init__( @@ -812,7 +809,7 @@ def __init__( return # Convert extract mesh - if isinstance(mesh, trimesh.primitives._Primitive): + if isinstance(mesh, trimesh.primitives.Primitive): self._mesh = mesh.to_mesh() elif isinstance(mesh, trimesh.base.Trimesh): self._mesh = mesh.copy() @@ -893,7 +890,6 @@ def sampleGiven(self, value): tolerance=self.tolerance, centerMesh=self.centerMesh, onDirection=self.onDirection, - engine=self.engine, name=self.name, ) @@ -920,7 +916,6 @@ def evaluateInner(self, context): tolerance=self.tolerance, centerMesh=self.centerMesh, onDirection=self.onDirection, - engine=self.engine, name=self.name, ) @@ -1057,7 +1052,6 @@ class MeshVolumeRegion(MeshRegion): tolerance: Tolerance for internal computations. centerMesh: Whether or not to center the mesh after copying and before transformations. onDirection: The direction to use if an object being placed on this region doesn't specify one. - engine: Which engine to use for mesh operations. Either "blender" or "scad". """ def __init__(self, *args, **kwargs): @@ -1414,12 +1408,7 @@ def intersect(self, other, triedReversed=False): other_mesh = other.mesh # Compute intersection using Trimesh - try: - new_mesh = self.mesh.intersection(other_mesh, engine=self.engine) - except ValueError as exc: - raise ValueError( - "Unable to compute mesh boolean operation. Do you have the Blender and OpenSCAD installed on your system?" - ) from exc + new_mesh = self.mesh.intersection(other_mesh) if new_mesh.is_empty: return nowhere @@ -1428,7 +1417,6 @@ def intersect(self, other, triedReversed=False): new_mesh, tolerance=min(self.tolerance, other.tolerance), centerMesh=False, - engine=self.engine, ) else: # Something went wrong, abort @@ -1625,12 +1613,7 @@ def union(self, other, triedReversed=False): other_mesh = other.mesh # Compute union using Trimesh - try: - new_mesh = self.mesh.union(other_mesh, engine=self.engine) - except ValueError as exc: - raise ValueError( - "Unable to compute mesh boolean operation. Do you have the Blender and OpenSCAD installed on your system?" - ) from exc + new_mesh = self.mesh.union(other_mesh) if new_mesh.is_empty: return nowhere @@ -1639,7 +1622,6 @@ def union(self, other, triedReversed=False): new_mesh, tolerance=min(self.tolerance, other.tolerance), centerMesh=False, - engine=self.engine, ) else: # Something went wrong, abort @@ -1665,14 +1647,7 @@ def difference(self, other, debug=False): other_mesh = other.mesh # Compute difference using Trimesh - try: - new_mesh = self.mesh.difference( - other_mesh, engine=self.engine, debug=debug - ) - except ValueError as exc: - raise ValueError( - "Unable to compute mesh boolean operation. Do you have the Blender and OpenSCAD installed on your system?" - ) from exc + new_mesh = self.mesh.difference(other_mesh) if new_mesh.is_empty: return nowhere @@ -1681,7 +1656,6 @@ def difference(self, other, debug=False): new_mesh, tolerance=min(self.tolerance, other.tolerance), centerMesh=False, - engine=self.engine, ) else: # Something went wrong, abort @@ -1761,7 +1735,6 @@ def getSurfaceRegion(self): tolerance=self.tolerance, centerMesh=False, onDirection=self.onDirection, - engine=self.engine, ) def getVolumeRegion(self): @@ -1953,7 +1926,6 @@ def getVolumeRegion(self): tolerance=self.tolerance, centerMesh=False, onDirection=self.onDirection, - engine=self.engine, ) def getSurfaceRegion(self): @@ -1985,7 +1957,6 @@ def sampleGiven(self, value): rotation=value[self.rotation], orientation=value[self.orientation], tolerance=self.tolerance, - engine=self.engine, name=self.name, ) @@ -2001,7 +1972,6 @@ def evaluateInner(self, context): rotation=rotation, orientation=orientation, tolerance=self.tolerance, - engine=self.engine, name=self.name, ) @@ -2030,7 +2000,6 @@ def sampleGiven(self, value): rotation=value[self.rotation], orientation=value[self.orientation], tolerance=self.tolerance, - engine=self.engine, name=self.name, ) @@ -2046,7 +2015,6 @@ def evaluateInner(self, context): rotation=rotation, orientation=orientation, tolerance=self.tolerance, - engine=self.engine, name=self.name, ) @@ -3626,9 +3594,7 @@ def __init__( view_region = None diameter = 2 * visibleDistance - base_sphere = SpheroidRegion( - dimensions=(diameter, diameter, diameter), engine="scad" - ) + base_sphere = SpheroidRegion(dimensions=(diameter, diameter, diameter)) if math.pi - angleCutoff <= viewAngles[1]: # Case 1 diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index 06cc70c54..880137275 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -245,18 +245,13 @@ def test_mesh_surface_region_negative_dimension(): MeshSurfaceRegion(mesh, dimensions=dims) -def test_mesh_operation_blender(): - r1 = BoxRegion(position=(0, 0, 0), dimensions=(1, 1, 1), engine="blender") - r2 = BoxRegion(position=(0, 0, 0), dimensions=(2, 2, 2), engine="blender") +def test_mesh_operation(): + r1 = BoxRegion(position=(0, 0, 0), dimensions=(2, 2, 2)) + r2 = BoxRegion(position=(0, 0, 0), dimensions=(1, 1, 1)) - r = r1.intersect(r2) - - -def test_mesh_operation_scad(): - r1 = BoxRegion(position=(0, 0, 0), dimensions=(1, 1, 1), engine="scad") - r2 = BoxRegion(position=(0, 0, 0), dimensions=(2, 2, 2), engine="scad") - - r = r1.intersect(r2) + r1.intersect(r2) + r1.union(r2) + r1.difference(r2) def test_mesh_volume_region_sampling(): @@ -288,19 +283,17 @@ def test_mesh_intersects(): def test_mesh_empty_intersection(): - for engine in ["blender", "scad"]: - r1 = BoxRegion(position=(0, 0, 0), engine=engine) - r2 = BoxRegion(position=(10, 10, 10), engine=engine) + r1 = BoxRegion(position=(0, 0, 0)) + r2 = BoxRegion(position=(10, 10, 10)) - assert isinstance(r1.intersect(r2), EmptyRegion) + assert isinstance(r1.intersect(r2), EmptyRegion) def test_mesh_empty_difference(): - for engine in ["blender", "scad"]: - r1 = BoxRegion(dimensions=(1, 1, 1), engine=engine) - r2 = BoxRegion(dimensions=(2, 2, 2), engine=engine) + r1 = BoxRegion(dimensions=(1, 1, 1)) + r2 = BoxRegion(dimensions=(2, 2, 2)) - assert isinstance(r1.difference(r2), EmptyRegion) + assert isinstance(r1.difference(r2), EmptyRegion) def test_path_region(): From 06f98c33c4cd2576903984d7eded0cfc98c55f11 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Fri, 10 Nov 2023 13:25:16 -0800 Subject: [PATCH 03/10] Work on updating view regions. --- src/scenic/core/regions.py | 54 +++++++++++++------------------------- tests/core/test_regions.py | 4 +-- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index f65816674..793b330de 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3567,10 +3567,7 @@ class ViewRegion(MeshVolumeRegion): * Case 2.a viewAngles[0] = 360 degrees => Sphere * Case 2.b viewAngles[0] < 360 degrees => Sphere & CylinderSectionRegion - * Case 2: viewAngles[1] < 180 degrees - - * Case 2.a viewAngles[0] = 360 degrees => Sphere - (Cone + Cone) (Cones on z axis expanding from origin) - * Case 2.b viewAngles[0] < 360 degrees => Sphere & ViewSectionRegion + * Case 2: viewAngles[1] < 180 degrees => Sphere & ViewSectionRegion When making changes to this class you should run ``pytest -k test_viewRegion --exhaustive``. @@ -3594,58 +3591,42 @@ def __init__( position=Vector(0, 0, 0), rotation=None, orientation=None, - angleCutoff=0.01, + angleCutoff=0.001, tolerance=1e-8, ): # Bound viewAngles from either side. if min(viewAngles) <= 0: raise ValueError("viewAngles cannot have a component less than or equal to 0") + if math.tau - angleCutoff <= viewAngles[0]: + viewAngles = (math.tau, viewAngles[1]) + + if math.pi - angleCutoff <= viewAngles[1]: + viewAngles = (viewAngles[0], math.pi) + view_region = None diameter = 2 * visibleDistance base_sphere = SpheroidRegion(dimensions=(diameter, diameter, diameter)) if math.pi - angleCutoff <= viewAngles[1]: # Case 1 - if math.tau - angleCutoff <= viewAngles[0]: + if viewAngles[0] == math.tau: # Case 1.a view_region = base_sphere else: + # Case 1.b view_region = base_sphere.intersect( CylinderSectionRegion(visibleDistance, viewAngles[0]) ) else: # Case 2 - if math.tau - angleCutoff <= viewAngles[0]: - # Case 2.a - # Create cone with yaw oriented around (0,0,-1) - padded_height = visibleDistance * 2 - radius = padded_height * math.tan((math.pi - viewAngles[1]) / 2) - - cone_mesh = trimesh.creation.cone(radius=radius, height=padded_height) - - position_matrix = translation_matrix((0, 0, -1 * padded_height)) - cone_mesh.apply_transform(position_matrix) - - # Create two cones around the yaw axis - orientation_1 = Orientation._fromEuler(0, 0, 0) - orientation_2 = Orientation._fromEuler(0, 0, math.pi) - - cone_1 = MeshVolumeRegion( - mesh=cone_mesh, rotation=orientation_1, centerMesh=False - ) - cone_2 = MeshVolumeRegion( - mesh=cone_mesh, rotation=orientation_2, centerMesh=False - ) - - view_region = base_sphere.difference(cone_1).difference(cone_2) - else: - # Case 2.b - view_region = base_sphere.intersect( - ViewSectionRegion(visibleDistance, viewAngles) - ) + view_region = base_sphere.intersect( + ViewSectionRegion(visibleDistance, viewAngles, angleCutoff) + ) assert view_region is not None + assert isinstance(view_region, MeshVolumeRegion) + assert view_region.containsPoint(Vector(0, 0, 0)) # Initialize volume region super().__init__( @@ -3683,8 +3664,9 @@ def __init__(self, visibleDistance, viewAngles, rotation=None, resolution=32): triangles.append((bot_line[li], bot_line[li + 1], top_line[li + 1])) # Side triangles - triangles.append((bot_line[0], top_line[0], (0, 0, 0))) - triangles.append((top_line[-1], bot_line[-1], (0, 0, 0))) + if viewAngles[0] < math.tau: + triangles.append((bot_line[0], top_line[0], (0, 0, 0))) + triangles.append((top_line[-1], bot_line[-1], (0, 0, 0))) # Top/Bottom triangles for li in range(len(top_line) - 1): diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index ddc607926..83e1cbc65 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -543,9 +543,9 @@ def test_pointset_region(): # ViewRegion tests -H_ANGLES = [0.1, 45, 90, 135, 179.9, 180, 180.1, 225, 270, 315, 359.9, 360] +H_ANGLES = [0.5, 45, 90, 135, 179.9, 180, 180.1, 225, 270, 315, 359.5, 360] -V_ANGLES = [0.1, 45, 90, 135, 179.9, 180] +V_ANGLES = [0.5, 45, 90, 135, 179.5, 180] VISIBLE_DISTANCES = [1, 25, 50] From 640433894d36776ea3867eff0bc03f263bcaa9a4 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 13 Dec 2023 15:42:56 -0800 Subject: [PATCH 04/10] Tweaked angle cutoff values. --- src/scenic/core/regions.py | 4 ++-- tests/core/test_regions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 793b330de..92acd55db 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3591,7 +3591,7 @@ def __init__( position=Vector(0, 0, 0), rotation=None, orientation=None, - angleCutoff=0.001, + angleCutoff=0.017, tolerance=1e-8, ): # Bound viewAngles from either side. @@ -3621,7 +3621,7 @@ def __init__( else: # Case 2 view_region = base_sphere.intersect( - ViewSectionRegion(visibleDistance, viewAngles, angleCutoff) + ViewSectionRegion(visibleDistance, viewAngles) ) assert view_region is not None diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index 83e1cbc65..ef8d3f7fe 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -543,9 +543,9 @@ def test_pointset_region(): # ViewRegion tests -H_ANGLES = [0.5, 45, 90, 135, 179.9, 180, 180.1, 225, 270, 315, 359.5, 360] +H_ANGLES = [1.01, 45, 90, 135, 179.99, 180, 180.01, 225, 270, 315, 358.99, 360] -V_ANGLES = [0.5, 45, 90, 135, 179.5, 180] +V_ANGLES = [1.01, 45, 90, 135, 178.99, 180] VISIBLE_DISTANCES = [1, 25, 50] From 98ec6d7c2d184e64b511b05ab3bf7cc25054440e Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Mon, 18 Dec 2023 15:36:27 -0800 Subject: [PATCH 05/10] View region patches. --- src/scenic/core/regions.py | 3 +++ tests/core/test_regions.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 92acd55db..c955565c4 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3598,6 +3598,9 @@ def __init__( if min(viewAngles) <= 0: raise ValueError("viewAngles cannot have a component less than or equal to 0") + # TODO True surface representation + viewAngles = (max(viewAngles[0], angleCutoff), max(viewAngles[1], angleCutoff)) + if math.tau - angleCutoff <= viewAngles[0]: viewAngles = (math.tau, viewAngles[1]) diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index ef8d3f7fe..c5fa83f1e 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -543,9 +543,9 @@ def test_pointset_region(): # ViewRegion tests -H_ANGLES = [1.01, 45, 90, 135, 179.99, 180, 180.01, 225, 270, 315, 358.99, 360] +H_ANGLES = [0.95, 45, 90, 135, 177.5, 180, 180.01, 225, 270, 315, 358.99, 360] -V_ANGLES = [1.01, 45, 90, 135, 178.99, 180] +V_ANGLES = [0.95, 45, 90, 135, 177.5, 180] VISIBLE_DISTANCES = [1, 25, 50] From 49697af4c76d17268eac6514f48246e71fb4775b Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 19 Dec 2023 14:44:33 -0800 Subject: [PATCH 06/10] Updated unifyMesh function. --- src/scenic/core/utils.py | 79 ++++++++++++++++++++-------------------- tests/core/test_utils.py | 20 +++++++++- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/scenic/core/utils.py b/src/scenic/core/utils.py index 9af12072c..557ea9380 100644 --- a/src/scenic/core/utils.py +++ b/src/scenic/core/utils.py @@ -163,10 +163,15 @@ def loadMesh(path, filetype, compressed, binary): def unifyMesh(mesh, verbose=False): - """Attempt to merge mesh bodies, aborting if something fails. + """Attempt to merge mesh bodies, raising an error if something fails. - Should only be used with meshes that are volumes. Returns the - original mesh if something goes wrong. + Should only be used with meshes that are volumes. + + If a mesh is composed of multiple bodies, the following process + is applied: + 1. Split mesh into volumes and holes. + 2. From each volume, subtract each hole that is fully contained. + 3. Union all the resulting volumes. """ assert mesh.is_volume @@ -176,30 +181,33 @@ def unifyMesh(mesh, verbose=False): mesh_bodies = mesh.split() - if not all(m.is_volume for m in mesh_bodies): - if verbose: - warnings.warn( - "The mesh that you loaded was composed of multiple bodies," - " but Scenic was unable to unify it because some of those bodies" - " are non-volumetric (e.g. hollow portions of a volume). This is probably" - " not an issue, but note that if any of these bodies have" - " intersecting faces, Scenic may give undefined resuls. To suppress" - " this warning in the future, consider adding the 'unify=False' parameter" - " to your fromFile call." - ) - return mesh - - try: + if all(m.is_volume for m in mesh_bodies): + # If all mesh bodies are volumes, we can just return the union. unified_mesh = trimesh.boolean.union(mesh_bodies) - except CalledProcessError: - # Something went wrong, return the original mesh - if verbose: - warnings.warn( - "The mesh that you loaded was composed of multiple bodies," - " but Scenic was unable to unify it because OpenSCAD raised" - " an error." - ) - return mesh + + else: + # Split the mesh bodies into volumes and holes. + volumes = [] + holes = [] + for m in mesh_bodies: + if m.is_volume: + volumes.append(m) + else: + m.fix_normals() + assert m.is_volume + holes.append(m) + + # For each volume, subtract all holes fully contained in the volume. + differenced_volumes = [] + + for v in volumes: + for h in filter(lambda h: h.volume < v.volume, holes): + if h.difference(v).is_empty: + v = v.difference(h) + differenced_volumes.append(v) + + # Union all the differenced volumes together. + unified_mesh = trimesh.boolean.union(differenced_volumes) # Check that the output is still a valid mesh if unified_mesh.is_volume: @@ -207,30 +215,23 @@ def unifyMesh(mesh, verbose=False): if unified_mesh.body_count == 1: warnings.warn( "The mesh that you loaded was composed of multiple bodies," - " but Scenic was able to unify it into one single body. To save on compile" + " but Scenic was able to unify it into one single body (though" + " you should verify that the result is correct). To save on compile" " time in the future, consider running unifyMesh on your mesh outside" " of Scenic and using that output instead." ) elif unified_mesh.body_count < mesh.body_count: warnings.warn( "The mesh that you loaded was composed of multiple bodies," - " but Scenic was able to unify it into fewer bodies. To save on compile" + " but Scenic was able to unify it into fewer bodies (though" + " you should verify that the result is correct). To save on compile" " time in the future, consider running unifyMesh on your mesh outside" - " of Scenic and using that output instead. Note that if any of these" - " bodies have intersecting faces, Scenic may give undefined resuls." + " of Scenic and using that output instead." ) return unified_mesh else: - if verbose: - warnings.warn( - "The mesh that you loaded was composed of multiple bodies," - " and Scenic was unable to unify it into fewer bodies. To save on compile" - " time in the future, consider adding the 'unify=False' parameter to your" - " fromFile call. Note that if any of these bodies have intersecting faces," - " Scenic may give undefined resuls." - ) - return mesh + raise ValueError("Unable to unify mesh.") def repairMesh(mesh, pitch=(1 / 2) ** 6, verbose=True): diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index f82d44799..65c6bf7a6 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -4,7 +4,7 @@ import pytest import trimesh -from scenic.core.utils import loadMesh, repairMesh +from scenic.core.utils import loadMesh, repairMesh, unifyMesh @pytest.mark.slow @@ -46,3 +46,21 @@ def test_mesh_repair(getAssetPath): with pytest.raises(ValueError): repairMesh(plane) + + +@pytest.mark.slow +def test_unify_mesh(): + # Create nested sphere + nested_sphere = ( + trimesh.creation.icosphere(radius=5) + .difference(trimesh.creation.icosphere(radius=4)) + .union(trimesh.creation.icosphere(radius=3)) + .difference(trimesh.creation.icosphere(radius=2)) + ) + + # Manually append a box + bad_mesh = trimesh.util.concatenate( + nested_sphere, trimesh.creation.box(bounds=((0, 0, 0), (3, 5, 3))) + ) + + unifyMesh(bad_mesh) From 21ee67306a6c796a95fbbc91d93d4f57b753582a Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 19 Dec 2023 14:49:38 -0800 Subject: [PATCH 07/10] Removed Blender/OpenSCAD install in CI. --- .github/workflows/run-tests.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 704bed0ea..bb0b3d1cf 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -51,35 +51,6 @@ jobs: with: ref: ${{ github.ref }} - - name: Install non-Python dependencies (Linux) - if: ${{ matrix.os == 'ubuntu-latest' }} - uses: awalsh128/cache-apt-pkgs-action@latest - with: - packages: blender openscad - - - name: Restore cached non-Python dependencies (Windows) - id: windows-cache-deps - if: ${{ matrix.os == 'windows-latest' }} - uses: actions/cache@v3 - with: - path: downloads - key: windows-deps - - - name: Download non-Python dependencies (Windows) - if: ${{ matrix.os == 'windows-latest' && steps.windows-cache-deps.outputs.cache-hit != 'true' }} - run: | - New-Item -Path downloads -ItemType Directory -Force - Invoke-WebRequest https://github.com/openscad/openscad/releases/download/openscad-2021.01/OpenSCAD-2021.01-x86-64.zip -O downloads/openscad.zip - Invoke-WebRequest https://download.blender.org/release/Blender3.6/blender-3.6.0-windows-x64.zip -O downloads/blender.zip - - - name: Install non-Python dependencies (Windows) - if: ${{ matrix.os == 'windows-latest' }} - run: | - Expand-Archive -Path downloads/openscad.zip -DestinationPath openscad - Move-Item -Path openscad/openscad-2021.01 -Destination $Env:Programfiles\OpenSCAD - Expand-Archive -Path downloads/blender.zip -DestinationPath blender - Move-Item -Path blender/blender-3.6.0-windows-x64 -Destination "$Env:Programfiles\Blender Foundation\Blender" - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: From 85e33f15cf0dba450d40aecd9992c5eab64b69fc Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Tue, 19 Dec 2023 15:27:05 -0800 Subject: [PATCH 08/10] Updated docs to remove references to OpenSCAD/Blender. --- docs/_templates/installation.rst | 2 +- docs/quickstart.rst | 14 ++++---------- docs/reference/region_types.rst | 2 -- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/docs/_templates/installation.rst b/docs/_templates/installation.rst index daedbf981..bd1af936f 100644 --- a/docs/_templates/installation.rst +++ b/docs/_templates/installation.rst @@ -1,4 +1,4 @@ -Next, activate the `virtual environment `_ in which you want to install Scenic. +Activate the `virtual environment `_ in which you want to install Scenic. To create and activate a new virtual environment called :file:`venv`, you can run the following commands: .. venv-setup-start diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2fe5bf7b5..34fe517d0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -25,20 +25,16 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions .. tab:: macOS - Start by downloading `Blender `__ and `OpenSCAD `__ and installing them into your :file:`Applications` directory. - .. include:: _templates/installation.rst .. tab:: Linux - Start by installing the Python-Tk interface, Blender, and OpenSCAD. + Start by installing the Python-Tk interface. You can likely use your system's package manager; e.g. on Debian/Ubuntu run: .. code-block:: text - sudo apt-get install python3-tk blender openscad - - For other Linux distributions or if you need to install from source, see the download pages for `Blender `__ and `OpenSCAD `__. + sudo apt-get install python3-tk .. include:: _templates/installation.rst @@ -46,8 +42,6 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions These instructions cover installing Scenic natively on Windows; if you are using the `Windows Subsystem for Linux `_ (on Windows 10 and newer), see the WSL tab instead. - Start by downloading and running the installers for `Blender `__ and `OpenSCAD `__. - .. include:: _templates/installation.rst :end-before: .. venv-setup-start @@ -64,12 +58,12 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions These instructions cover installing Scenic on the Windows Subsystem for Linux (WSL). If you haven't already installed WSL, you can do that by running :command:`wsl --install` (in either Command Prompt or PowerShell) and restarting your computer. - Then open a WSL terminal and run the following commands to install Python, the Python-Tk interface, Blender, and OpenSCAD: + Then open a WSL terminal and run the following commands to install Python and the Python-Tk interface: .. code-block:: text sudo apt-get update - sudo apt-get install python3 python3-tk blender openscad + sudo apt-get install python3 python3-tk .. include:: _templates/installation.rst :end-before: .. venv-setup-start diff --git a/docs/reference/region_types.rst b/docs/reference/region_types.rst index ffe44d1aa..76507693c 100644 --- a/docs/reference/region_types.rst +++ b/docs/reference/region_types.rst @@ -78,8 +78,6 @@ When checking containment of an `Object` in a 2D region, Scenic will atuomatical Most 3D regions inherit from either `MeshVolumeRegion` or `MeshSurfaceRegion`, which represent the volume (of a watertight mesh) and the surface of a mesh respectively. Various region classes are also provided to create primitive shapes. `MeshVolumeRegion` can be converted to `MeshSurfaceRegion` (and vice versa) using the the ``getSurfaceRegion`` and ``getVolumeRegion`` methods. -Mesh regions can use one of two engines for mesh operations: Blender or OpenSCAD. This can be controlled using the ``engine`` parameter, passing ``"blender"`` or ``"scad"`` respectively. Blender is generally more tolerant but can produce unreliable output, such as meshes that have microscopic holes. OpenSCAD is generally more precise, but may crash on certain inputs that it considers ill-defined. By default, Scenic uses Blender internally. - PolygonalFootprintRegions represent the :term:`footprint` of a 2D region. See `2D Regions` for more details. .. autoclass:: scenic.core.regions.MeshVolumeRegion From b472b3a3d209d42c48448c027b4f678fdf4009c1 Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Wed, 10 Jan 2024 18:29:52 -0800 Subject: [PATCH 09/10] ManifoldEngine PR Fixes. --- src/scenic/core/regions.py | 4 ++-- src/scenic/core/utils.py | 16 ++++++++++++++-- tests/core/test_utils.py | 4 +++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index c955565c4..c75525e83 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3564,8 +3564,8 @@ class ViewRegion(MeshVolumeRegion): * Case 1: viewAngles[1] = 180 degrees - * Case 2.a viewAngles[0] = 360 degrees => Sphere - * Case 2.b viewAngles[0] < 360 degrees => Sphere & CylinderSectionRegion + * Case 1.a viewAngles[0] = 360 degrees => Sphere + * Case 1.b viewAngles[0] < 360 degrees => Sphere & CylinderSectionRegion * Case 2: viewAngles[1] < 180 degrees => Sphere & ViewSectionRegion diff --git a/src/scenic/core/utils.py b/src/scenic/core/utils.py index 557ea9380..8e234f13a 100644 --- a/src/scenic/core/utils.py +++ b/src/scenic/core/utils.py @@ -163,7 +163,7 @@ def loadMesh(path, filetype, compressed, binary): def unifyMesh(mesh, verbose=False): - """Attempt to merge mesh bodies, raising an error if something fails. + """Attempt to merge mesh bodies, raising a `ValueError` if something fails. Should only be used with meshes that are volumes. @@ -197,15 +197,27 @@ def unifyMesh(mesh, verbose=False): assert m.is_volume holes.append(m) - # For each volume, subtract all holes fully contained in the volume. + # For each volume, subtract all holes fully contained in the volume, + # keeping track of which holes are fully contained in at least one solid. differenced_volumes = [] + contained_holes = set() for v in volumes: for h in filter(lambda h: h.volume < v.volume, holes): if h.difference(v).is_empty: + contained_holes.add(h) v = v.difference(h) differenced_volumes.append(v) + # If one or more holes was not fully contained (and thus ignored), + # raise a warning. + if verbose: + if contained_holes != set(holes): + warnings.warn( + "One or more holes in the provided mesh was not fully contained" + " in any solid (and was ignored)." + ) + # Union all the differenced volumes together. unified_mesh = trimesh.boolean.union(differenced_volumes) diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 65c6bf7a6..535aa11ea 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -63,4 +63,6 @@ def test_unify_mesh(): nested_sphere, trimesh.creation.box(bounds=((0, 0, 0), (3, 5, 3))) ) - unifyMesh(bad_mesh) + fixed_mesh = unifyMesh(bad_mesh) + assert fixed_mesh.is_volume + assert fixed_mesh.body_count == 1 From d40cd687be571125e3ff9fdda49d6d1c40a6f9fd Mon Sep 17 00:00:00 2001 From: Eric Vin Date: Thu, 11 Jan 2024 21:19:21 -0800 Subject: [PATCH 10/10] Fixes for manifold api change. --- pyproject.toml | 4 ++-- tests/core/test_utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c556d8344..d1e16e0db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "dotmap ~= 1.3", "mapbox_earcut >= 0.12.10", "matplotlib ~= 3.2", - "manifold3d ~= 2.2", + "manifold3d == 2.3.0", "networkx >= 2.6", "numpy ~= 1.24", "opencv-python ~= 4.5", @@ -47,7 +47,7 @@ dependencies = [ "scikit-image ~= 0.21", "scipy ~= 1.7", "shapely ~= 2.0", - "trimesh >=4.0.3, <5", + "trimesh >=4.0.9, <5", ] [project.optional-dependencies] diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 535aa11ea..de2f8e6a7 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -65,4 +65,4 @@ def test_unify_mesh(): fixed_mesh = unifyMesh(bad_mesh) assert fixed_mesh.is_volume - assert fixed_mesh.body_count == 1 + assert fixed_mesh.body_count == 3