init
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from . import blender
|
||||
|
||||
__all__ = ["blender"]
|
||||
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from .. import util
|
||||
from ..constants import log
|
||||
from ..resources import get_string
|
||||
from ..typed import BooleanOperationType, Iterable
|
||||
from .generic import MeshScript
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# try to find Blender install on Windows
|
||||
# split existing path by delimiter
|
||||
_search_path = [i for i in os.environ.get("PATH", "").split(";") if len(i) > 0]
|
||||
for pf in [r"C:\Program Files", r"C:\Program Files (x86)"]:
|
||||
pf = os.path.join(pf, "Blender Foundation")
|
||||
if os.path.exists(pf):
|
||||
for p in os.listdir(pf):
|
||||
if "Blender" in p:
|
||||
_search_path.append(os.path.join(pf, p))
|
||||
_search_path = ";".join(set(_search_path))
|
||||
log.debug("searching for blender in: %s", _search_path)
|
||||
elif platform.system() == "Darwin":
|
||||
# try to find Blender on Mac OSX
|
||||
_search_path = [i for i in os.environ.get("PATH", "").split(":") if len(i) > 0]
|
||||
_search_path.extend(
|
||||
[
|
||||
"/Applications/blender.app/Contents/MacOS",
|
||||
"/Applications/Blender.app/Contents/MacOS",
|
||||
"/Applications/Blender/blender.app/Contents/MacOS",
|
||||
]
|
||||
)
|
||||
_search_path = ":".join(set(_search_path))
|
||||
log.debug("searching for blender in: %s", _search_path)
|
||||
else:
|
||||
_search_path = os.environ.get("PATH", "")
|
||||
|
||||
_blender_executable = util.which("blender", path=_search_path)
|
||||
exists = _blender_executable is not None
|
||||
|
||||
# a map that translates:
|
||||
# `trimesh.BooleanOperationType` -> blender value
|
||||
_blender_bool = {
|
||||
"union": "UNION",
|
||||
"difference": "DIFFERENCE",
|
||||
"intersection": "INTERSECT",
|
||||
}
|
||||
|
||||
|
||||
def boolean(
|
||||
meshes: Iterable,
|
||||
operation: BooleanOperationType = "difference",
|
||||
use_exact: bool = True,
|
||||
use_self: bool = False,
|
||||
debug: bool = False,
|
||||
check_volume: bool = True,
|
||||
):
|
||||
"""
|
||||
Run a boolean operation with multiple meshes using Blender.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
meshes
|
||||
List of mesh objects to be operated on
|
||||
operation
|
||||
Type of boolean operation ("difference", "union", "intersect").
|
||||
use_exact
|
||||
Use the "exact" mode as opposed to the "fast" mode.
|
||||
use_self
|
||||
Whether to consider self-intersections.
|
||||
debug
|
||||
Provide additional output for troubleshooting.
|
||||
check_volume
|
||||
Raise an error if not all meshes are watertight
|
||||
positive volumes. Advanced users may want to ignore
|
||||
this check as it is expensive.
|
||||
|
||||
Returns
|
||||
----------
|
||||
result
|
||||
The result of the boolean operation on the provided meshes.
|
||||
"""
|
||||
if not exists:
|
||||
raise ValueError("No blender available!")
|
||||
if check_volume and not all(m.is_volume for m in meshes):
|
||||
raise ValueError("Not all meshes are volumes!")
|
||||
|
||||
# conversions from the trimesh `BooleanOperationType` to the blender option
|
||||
key = operation.lower().strip()
|
||||
if key not in _blender_bool:
|
||||
raise ValueError(
|
||||
f"`{operation}` is not a valid boolean: `{_blender_bool.keys()}`"
|
||||
)
|
||||
|
||||
if use_exact:
|
||||
solver_options = "EXACT"
|
||||
else:
|
||||
solver_options = "FAST"
|
||||
|
||||
# get the template from our resources folder
|
||||
template = get_string("templates/blender_boolean.py.tmpl")
|
||||
# use string substitutions rather than `string.Template` as we aren't going
|
||||
# to be filling in all the values here, `MeshScript` is going to be
|
||||
# the source of `$MESH_PRE`, etc.
|
||||
script = (
|
||||
template.replace("$OPERATION", _blender_bool[key])
|
||||
.replace("$SOLVER_OPTIONS", solver_options)
|
||||
.replace("$USE_SELF", f"{use_self}")
|
||||
)
|
||||
with MeshScript(meshes=meshes, script=script, debug=debug) as blend:
|
||||
result = blend.run(_blender_executable + " --background --python $SCRIPT")
|
||||
|
||||
result = util.make_sequence(result)
|
||||
for m in result:
|
||||
# blender returns actively incorrect face normals
|
||||
m.face_normals = None
|
||||
|
||||
return util.concatenate(result)
|
||||
|
||||
|
||||
def unwrap(
|
||||
mesh, angle_limit: float = 66.0, island_margin: float = 0.0, debug: bool = False
|
||||
):
|
||||
"""
|
||||
Run an unwrap operation using blender.
|
||||
"""
|
||||
if not exists:
|
||||
raise ValueError("No blender available!")
|
||||
|
||||
# get the template from our resources folder
|
||||
template = get_string("templates/blender_unwrap.py.template")
|
||||
script = template.replace("$ANGLE_LIMIT", f"{angle_limit:.6f}").replace(
|
||||
"$ISLAND_MARGIN", f"{island_margin:.6f}"
|
||||
)
|
||||
|
||||
with MeshScript(meshes=[mesh], script=script, exchange="obj", debug=debug) as blend:
|
||||
result = blend.run(_blender_executable + " --background --python $SCRIPT")
|
||||
|
||||
for m in util.make_sequence(result):
|
||||
# blender returns actively incorrect face normals
|
||||
m.face_normals = None
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from string import Template
|
||||
from subprocess import CalledProcessError, check_output
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from .. import exchange
|
||||
from ..util import log
|
||||
|
||||
|
||||
class MeshScript:
|
||||
def __init__(self, meshes, script, exchange="stl", debug=False, **kwargs):
|
||||
self.debug = debug
|
||||
self.kwargs = kwargs
|
||||
self.meshes = meshes
|
||||
self.script = script
|
||||
self.exchange = exchange
|
||||
|
||||
def __enter__(self):
|
||||
# windows has problems with multiple programs using open files so we close
|
||||
# them at the end of the enter call, and delete them ourselves at exit
|
||||
# Blender sorts its objects alphabetically
|
||||
# so prefix the mesh number on the file name
|
||||
digit_count = len(str(len(self.meshes)))
|
||||
self.mesh_pre = [
|
||||
NamedTemporaryFile(
|
||||
suffix=f".{self.exchange}",
|
||||
prefix=f"{str(i).zfill(digit_count)}_",
|
||||
mode="wb",
|
||||
delete=False,
|
||||
)
|
||||
for i in range(len(self.meshes))
|
||||
]
|
||||
self.mesh_post = NamedTemporaryFile(
|
||||
suffix=f".{self.exchange}", mode="rb", delete=False
|
||||
)
|
||||
self.script_out = NamedTemporaryFile(mode="wb", delete=False)
|
||||
|
||||
# export the meshes to a temporary STL container
|
||||
for mesh, file_obj in zip(self.meshes, self.mesh_pre):
|
||||
mesh.export(file_obj=file_obj.name)
|
||||
|
||||
self.replacement = {"MESH_" + str(i): m.name for i, m in enumerate(self.mesh_pre)}
|
||||
self.replacement["MESH_PRE"] = str([i.name for i in self.mesh_pre])
|
||||
self.replacement["MESH_POST"] = self.mesh_post.name
|
||||
self.replacement["SCRIPT"] = self.script_out.name
|
||||
|
||||
script_text = Template(self.script).substitute(self.replacement)
|
||||
if platform.system() == "Windows":
|
||||
script_text = script_text.replace("\\", "\\\\")
|
||||
self.script_out.write(script_text.encode("utf-8"))
|
||||
|
||||
# close all temporary files
|
||||
self.script_out.close()
|
||||
self.mesh_post.close()
|
||||
for file_obj in self.mesh_pre:
|
||||
file_obj.close()
|
||||
return self
|
||||
|
||||
def run(self, command):
|
||||
command_run = Template(command).substitute(self.replacement).split()
|
||||
# run the binary
|
||||
startupinfo = None
|
||||
if platform.system() == "Windows":
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
||||
if self.debug:
|
||||
log.info("executing: {}".format(" ".join(command_run)))
|
||||
|
||||
try:
|
||||
output = check_output(
|
||||
command_run, stderr=subprocess.STDOUT, startupinfo=startupinfo
|
||||
)
|
||||
except CalledProcessError as E:
|
||||
# raise with the output from the process
|
||||
raise RuntimeError(E.output.decode())
|
||||
|
||||
if self.debug:
|
||||
log.info(output.decode())
|
||||
|
||||
# bring the binaries result back as a set of Trimesh kwargs
|
||||
mesh_results = exchange.load.load_mesh(self.mesh_post.name, **self.kwargs)
|
||||
|
||||
return mesh_results
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
if self.debug:
|
||||
log.info(f"MeshScript.debug: not deleting {self.script_out.name}")
|
||||
return
|
||||
# delete all the temporary files by name
|
||||
# they are closed but their names are still available
|
||||
os.remove(self.script_out.name)
|
||||
for file_obj in self.mesh_pre:
|
||||
os.remove(file_obj.name)
|
||||
os.remove(self.mesh_post.name)
|
||||
Reference in New Issue
Block a user