from coperniFUS import *
[docs]
class TrimeshHandler:
_DEFAULT_PARAMS = {
'ignore_anatomical_landmarks_calibration': True,
'ignore_plane_slicing': False,
'gl_mesh_shader': 'viewNormalColor',
'gl_mesh_drawEdges': False,
'gl_mesh_drawFaces': True,
'gl_mesh_edgeColor': (.5, .5, .5, .7),
'gl_mesh_glOptions': 'opaque',
'gl_mesh_smooth': False,
'gl_mesh_edgeWidth': 5,
}
""" Default configuration parameters used when a parameter value is not yet cached """
def __init__(self, parent_viewer, **kwargs) -> None:
self.parent_viewer = parent_viewer
self.stl_item_name = None
self._stl_item_tmat = None
self._raw_stl_item_mesh = None
self._stl_item_mesh_processed = None
self._sliced_stl_item_mesh = None
self._stl_item_mesh = None
self.stl_glitem = None
# --- Public attributes ---
@property
def stl_item_mesh_processed(self):
""" Mesh object with affine transformation and boolean operations (if applicable) applied """
if self._stl_item_mesh_processed is None:
return None
if np.all(self._stl_item_mesh_processed[0] == self.stl_item_tmat):
return self._stl_item_mesh_processed[1] # Return mesh (or list of meshes) only
else: # Reset if the boolean transform is no longer up to date
self._stl_item_mesh_processed = None
return None
@stl_item_mesh_processed.setter
def stl_item_mesh_processed(self, mesh):
# Store mesh + current transform matrix (track tf change)
self._stl_item_mesh_processed = (self.stl_item_tmat, mesh)
@property
def stl_item_mesh(self):
""" Mesh object with affine transformation applied """
def apply_tr(raw_mesh):
mesh = copy.deepcopy(raw_mesh)
mesh.apply_transform(self.stl_item_tmat.T)
return mesh
if self.stl_item_mesh_processed is not None:
return self.stl_item_mesh_processed
if self._stl_item_mesh is None:
if isinstance(self.raw_stl_item_mesh, trimesh.Trimesh):
self._stl_item_mesh = apply_tr(self.raw_stl_item_mesh)
elif isinstance(self.raw_stl_item_mesh, list):
self._stl_item_mesh = [apply_tr(mm) for mm in self.raw_stl_item_mesh]
# Ignore if None
return self._stl_item_mesh
@stl_item_mesh.setter
def stl_item_mesh(self, value):
self._stl_item_mesh = value
@property
def raw_stl_item_mesh(self):
""" Raw mesh object without any affine transformations or processing applied """
if self._raw_stl_item_mesh is None:
self.stl_item_mesh = None # Reset inheriting processed mesh
return self._raw_stl_item_mesh
@raw_stl_item_mesh.setter
def raw_stl_item_mesh(self, value):
self._raw_stl_item_mesh = value
@property
def stl_item_tmat(self):
""" Holds the affine transformation matrix for the trimesh object """
if self._stl_item_tmat is None:
self._stl_item_tmat = np.eye(4) # No transform as default
# Apply anatomical landmark calibration transformation if enabled
if not self.get_user_param('ignore_anatomical_landmarks_calibration'):
anatomically_calibrated_stl_item_tmat = self._stl_item_tmat @ self.parent_viewer.anat_calib.landmarks_calib_tmat
else:
anatomically_calibrated_stl_item_tmat = self._stl_item_tmat
return anatomically_calibrated_stl_item_tmat
@stl_item_tmat.setter
def stl_item_tmat(self, value):
if value is not None:
if value.shape != (4, 4):
raise ValueError('Transformation matrix should be of shape (4, 4)')
self._stl_item_tmat = value
self.stl_item_mesh = None # Reset processed stl mesh to apply transform
# --- Required attributes for rendering ---
[docs]
def add_rendered_object(self):
""" Called when populating the viewer with the module rendered objects """
def add_mesh_render(mesh):
stl_item_gl_mesh_data = gl.MeshData(vertexes=mesh.vertices, faces=mesh.faces)
self.stl_glitem.append(
gl.GLMeshItem(
meshdata=stl_item_gl_mesh_data,
shader=self.get_user_param('gl_mesh_shader'), # TODO get_user_param -> redondant -> transfer to stl dock
smooth=self.get_user_param('gl_mesh_smooth'),
drawFaces=self.get_user_param('gl_mesh_drawFaces'),
drawEdges=self.get_user_param('gl_mesh_drawEdges'),
edgeColor=self.get_user_param('gl_mesh_edgeColor'),
edgeWidth=self.get_user_param('gl_mesh_edgeWidth'),
glOptions=self.get_user_param('gl_mesh_glOptions'),
))
bool_mesh_index_str = f' {mesh.bool_mesh_index}' if hasattr(mesh, 'bool_mesh_index') else ''
self.parent_viewer.gl_view.addItem(self.stl_glitem[-1], name=f'{self.stl_item_name}{bool_mesh_index_str} STL mesh')
self.stl_glitem[-1].setDepthValue(-1)
self.stl_glitem = []
if isinstance(self.stl_item_mesh, trimesh.Trimesh):
add_mesh_render(self.stl_item_mesh)
elif isinstance(self.stl_item_mesh, list):
for mm in self.stl_item_mesh:
add_mesh_render(mm)
# Ignore if None
[docs]
def update_rendered_object(self):
""" Called on render view updates """
def update_rendered_mesh(mesh, sub_mesh_index=0):
if self.parent_viewer.slicing_plane_normal_vect is None or ignore_plane_slicing:
stl_item_gl_mesh_data = gl.MeshData(vertexes=mesh.vertices, faces=mesh.faces)
else:
self._sliced_stl_item_mesh = mesh.slice_plane(
plane_origin=self.parent_viewer.slicing_plane_normal_vect[0],
plane_normal=self.parent_viewer.slicing_plane_normal_vect[1],
cap=True)
stl_item_gl_mesh_data = gl.MeshData(vertexes=self._sliced_stl_item_mesh.vertices, faces=self._sliced_stl_item_mesh.faces)
if sub_mesh_index < len(self.stl_glitem):
self.stl_glitem[sub_mesh_index].setMeshData(meshdata=stl_item_gl_mesh_data)
ignore_plane_slicing = self.get_user_param('ignore_plane_slicing')
if not self.parent_viewer._postpone_slicing_plane_computation or ignore_plane_slicing:
if self.stl_glitem is None:
self.add_rendered_object()
else:
if isinstance(self.stl_item_mesh, trimesh.Trimesh):
update_rendered_mesh(self.stl_item_mesh)
elif isinstance(self.stl_item_mesh, list):
for ii, mm in enumerate(self.stl_item_mesh):
update_rendered_mesh(mm, sub_mesh_index=ii)
# Ignore if None
[docs]
def delete_rendered_object(self):
""" Called on deletion of the module rendered objects """
if self.stl_glitem is not None:
if isinstance(self.stl_glitem, trimesh.Trimesh):
self.parent_viewer.gl_view.removeItem(self.stl_glitem)
elif isinstance(self.stl_glitem, list):
for mm in self.stl_glitem:
self.parent_viewer.gl_view.removeItem(mm)
self.raw_stl_item_mesh = None
self.stl_item_mesh = None
self.stl_glitem = None
# --- cache wrapper for interface parameters ---
[docs]
def get_user_param(self, param_name, default_value=None):
""" Get module configuration parameter stored in cache (or default values if non existant) """
if default_value is None and param_name in self._DEFAULT_PARAMS:
default_value = self._DEFAULT_PARAMS[param_name]
if self.stl_item_name is not None:
param_value = self.parent_viewer.cache.get_attr(
['mesh_handler', self.stl_item_name, param_name],
default_value = default_value
)
else:
param_value = default_value
return param_value
[docs]
def set_user_param(self, param_name, param_value):
""" Set module configuration parameter to cache """
if self.stl_item_name is not None:
self.parent_viewer.cache.set_attr(
['mesh_handler', self.stl_item_name, param_name],
param_value
)
class StlHandler(TrimeshHandler):
_DEFAULT_PARAMS = {
'file_path': 'None',
'ignore_anatomical_landmarks_calibration': True,
'ignore_plane_slicing': False,
'gl_mesh_shader': 'viewNormalColor',
'gl_mesh_drawEdges': False,
'gl_mesh_drawFaces': True,
'gl_mesh_edgeColor': (.5, .5, .5, .7),
'gl_mesh_glOptions': 'opaque',
'gl_mesh_smooth': False,
'gl_mesh_edgeWidth': 5,
}
""" Default configuration parameters used when a parameter value is not yet cached """
def __init__(self, parent_viewer, **kwargs) -> None:
super().__init__(parent_viewer, **kwargs)
@property
def raw_stl_item_mesh(self): # Override mesh import for stl
""" Raw mesh object without any affine transformations or processing applied """
if self._raw_stl_item_mesh is None:
self.stl_item_mesh = None # Reset inheriting processed mesh
stl_file_path = self.get_user_param('file_path')
if stl_file_path is not None and pathlib.Path(stl_file_path).exists():
self._raw_stl_item_mesh = trimesh.load(stl_file_path)
return self._raw_stl_item_mesh
@raw_stl_item_mesh.setter
def raw_stl_item_mesh(self, value):
self._raw_stl_item_mesh = value
[docs]
class TrimeshScriptHandler(TrimeshHandler):
def __init__(self, parent_viewer, **kwargs) -> None:
super().__init__(parent_viewer, **kwargs)
self._eval_scripted_mesh = None
self._trimesh_script = None
self._trimesh_script_constants_dict = None
@property
def trimesh_script(self): # Override mesh import for stl
""" Script to be used to construct the trimesh mesh. Uppon execution, the script should define a 'mesh' object. """
if self._trimesh_script is None:
self._trimesh_script = """"""
return self._trimesh_script
@trimesh_script.setter
def trimesh_script(self, value):
if value is None or isinstance(value, str):
self._trimesh_script = value
else:
raise ValueError('trimesh_script should be a string (or None).')
@property
def trimesh_script_constants_dict(self): # Override mesh import for stl
""" Constants to be provided uppon evaluation of the trimesh_script. Constants keys in trimesh_script_constants_dict can be referenced trimesh_script """
if self._trimesh_script_constants_dict is None:
self._trimesh_script_constants_dict = {}
return self._trimesh_script_constants_dict
@trimesh_script_constants_dict.setter
def trimesh_script_constants_dict(self, value):
if value is None or isinstance(value, dict):
self._trimesh_script_constants_dict = value
else:
raise ValueError('trimesh_script_constants_dict should be a dict (or None).')
[docs]
def eval_scripted_mesh(self):
""" Trimesh mesh object constructed from trimesh_script attriute. Uppon execution, the script should define a 'mesh' object. """
current_param_hash = ''
if self._eval_scripted_mesh is not None and isinstance(self._eval_scripted_mesh, tuple):
current_param_hash = self._eval_scripted_mesh[1]
if self.trimesh_script is not None and (self._eval_scripted_mesh is None or current_param_hash != object_list_hash([self.trimesh_script, self.trimesh_script_constants_dict])):
accessible_globals_names = [
'trimesh', 'np',
'dict_to_path_patched' # Depricated in v0.1.1
]
accessible_globals = {
accessible_glob_name: globals()[accessible_glob_name]
for accessible_glob_name in accessible_globals_names
}
accessible_globals = {
**accessible_globals, **self.trimesh_script_constants_dict,
'dict_to_path': trimesh.path.exchange.misc.dict_to_path
}
# run trimesh script
self._eval_scripted_mesh = (None, '') # mesh, param_hash
try:
exec(self.trimesh_script, accessible_globals)
if 'mesh' in accessible_globals:
scripted_mesh = accessible_globals['mesh']
scripted_mesh = ensure_mesh_is_a_volume_manifold(scripted_mesh) # Cleanup mesh
param_hash = object_list_hash([self.trimesh_script, self.trimesh_script_constants_dict])
self._eval_scripted_mesh = (scripted_mesh, param_hash)
except Exception as e:
raise ValueError(f'{type(e).__name__} in trimesh_script evaluation: {str(e)}')
return self._eval_scripted_mesh[0]
@property
def raw_stl_item_mesh(self): # Override mesh import for stl
""" Raw mesh object without any affine transformations or processing applied """
if self._raw_stl_item_mesh is None:
self.stl_item_mesh = None # Reset inheriting processed mesh
self._raw_stl_item_mesh = self.eval_scripted_mesh()
return self._raw_stl_item_mesh
@raw_stl_item_mesh.setter
def raw_stl_item_mesh(self, value):
self._raw_stl_item_mesh = value
[docs]
class StlHandler(TrimeshHandler):
_DEFAULT_PARAMS = {
'file_path': 'None',
'ignore_anatomical_landmarks_calibration': True,
'ignore_plane_slicing': False,
'gl_mesh_shader': 'viewNormalColor',
'gl_mesh_drawEdges': False,
'gl_mesh_drawFaces': True,
'gl_mesh_edgeColor': (.5, .5, .5, .7),
'gl_mesh_glOptions': 'opaque',
'gl_mesh_smooth': False,
'gl_mesh_edgeWidth': 5,
}
""" Default configuration parameters used when a parameter value is not yet cached """
def __init__(self, parent_viewer, **kwargs) -> None:
super().__init__(parent_viewer, **kwargs)
@property
def raw_stl_item_mesh(self): # Override mesh import for stl
""" Raw mesh object without any affine transformations or processing applied """
if self._raw_stl_item_mesh is None:
self.stl_item_mesh = None # Reset inheriting processed mesh
stl_file_path = self.get_user_param('file_path')
if stl_file_path is not None and pathlib.Path(stl_file_path).exists():
self._raw_stl_item_mesh = trimesh.load(stl_file_path)
return self._raw_stl_item_mesh
@raw_stl_item_mesh.setter
def raw_stl_item_mesh(self, value):
self._raw_stl_item_mesh = value