Skip to content

Commit

Permalink
Merge pull request #169 from BerkeleyLearnVerify/ManifoldEngine
Browse files Browse the repository at this point in the history
Move to Manifold for Boolean Operations
  • Loading branch information
dfremont authored Jan 12, 2024
2 parents 39b2279 + d40cd68 commit e18eb95
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 180 deletions.
29 changes: 0 additions & 29 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/_templates/installation.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Next, activate the `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ in which you want to install Scenic.
Activate the `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ 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
Expand Down
14 changes: 4 additions & 10 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,23 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions

.. tab:: macOS

Start by downloading `Blender <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__ 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 <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__.
sudo apt-get install python3-tk
.. include:: _templates/installation.rst

.. tab:: Windows

These instructions cover installing Scenic natively on Windows; if you are using the `Windows Subsystem for Linux <https://docs.microsoft.com/en-us/windows/wsl/install-win10>`_ (on Windows 10 and newer), see the WSL tab instead.

Start by downloading and running the installers for `Blender <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__.

.. include:: _templates/installation.rst
:end-before: .. venv-setup-start

Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions docs/reference/region_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies = [
"dotmap ~= 1.3",
"mapbox_earcut >= 0.12.10",
"matplotlib ~= 3.2",
"manifold3d == 2.3.0",
"networkx >= 2.6",
"numpy ~= 1.24",
"opencv-python ~= 4.5",
Expand All @@ -46,7 +47,7 @@ dependencies = [
"scikit-image ~= 0.21",
"scipy ~= 1.7",
"shapely ~= 2.0",
"trimesh >=3.22.5, <4",
"trimesh >=4.0.9, <5",
]

[project.optional-dependencies]
Expand Down
105 changes: 28 additions & 77 deletions src/scenic/core/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -781,7 +780,6 @@ def __init__(
tolerance=1e-6,
centerMesh=True,
onDirection=None,
engine="scad",
name=None,
additionalDeps=[],
):
Expand All @@ -794,7 +792,6 @@ def __init__(
self.tolerance = tolerance
self.centerMesh = centerMesh
self.onDirection = onDirection
self.engine = engine

# Initialize superclass with samplables
super().__init__(
Expand All @@ -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()
Expand Down Expand Up @@ -893,7 +890,6 @@ def sampleGiven(self, value):
tolerance=self.tolerance,
centerMesh=self.centerMesh,
onDirection=self.onDirection,
engine=self.engine,
name=self.name,
)

Expand All @@ -920,7 +916,6 @@ def evaluateInner(self, context):
tolerance=self.tolerance,
centerMesh=self.centerMesh,
onDirection=self.onDirection,
engine=self.engine,
name=self.name,
)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -1418,12 +1412,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
Expand All @@ -1432,7 +1421,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
Expand Down Expand Up @@ -1629,12 +1617,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
Expand All @@ -1643,7 +1626,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
Expand All @@ -1669,14 +1651,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
Expand All @@ -1685,7 +1660,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
Expand Down Expand Up @@ -1765,7 +1739,6 @@ def getSurfaceRegion(self):
tolerance=self.tolerance,
centerMesh=False,
onDirection=self.onDirection,
engine=self.engine,
)

def getVolumeRegion(self):
Expand Down Expand Up @@ -1957,7 +1930,6 @@ def getVolumeRegion(self):
tolerance=self.tolerance,
centerMesh=False,
onDirection=self.onDirection,
engine=self.engine,
)

def getSurfaceRegion(self):
Expand Down Expand Up @@ -1989,7 +1961,6 @@ def sampleGiven(self, value):
rotation=value[self.rotation],
orientation=value[self.orientation],
tolerance=self.tolerance,
engine=self.engine,
name=self.name,
)

Expand All @@ -2005,7 +1976,6 @@ def evaluateInner(self, context):
rotation=rotation,
orientation=orientation,
tolerance=self.tolerance,
engine=self.engine,
name=self.name,
)

Expand Down Expand Up @@ -2034,7 +2004,6 @@ def sampleGiven(self, value):
rotation=value[self.rotation],
orientation=value[self.orientation],
tolerance=self.tolerance,
engine=self.engine,
name=self.name,
)

Expand All @@ -2050,7 +2019,6 @@ def evaluateInner(self, context):
rotation=rotation,
orientation=orientation,
tolerance=self.tolerance,
engine=self.engine,
name=self.name,
)

Expand Down Expand Up @@ -3596,13 +3564,10 @@ 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 2: viewAngles[1] < 180 degrees
* Case 1.a viewAngles[0] = 360 degrees => Sphere
* Case 1.b viewAngles[0] < 360 degrees => Sphere & CylinderSectionRegion
* 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``.
Expand All @@ -3626,60 +3591,45 @@ def __init__(
position=Vector(0, 0, 0),
rotation=None,
orientation=None,
angleCutoff=0.01,
angleCutoff=0.017,
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")

# TODO True surface representation
viewAngles = (max(viewAngles[0], angleCutoff), max(viewAngles[1], angleCutoff))

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), engine="scad"
)
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)
)

assert view_region is not None
assert isinstance(view_region, MeshVolumeRegion)
assert view_region.containsPoint(Vector(0, 0, 0))

# Initialize volume region
super().__init__(
Expand Down Expand Up @@ -3717,8 +3667,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):
Expand Down
Loading

0 comments on commit e18eb95

Please sign in to comment.