# vtk_pipeline_wx.py
import threading
import numpy as np
import wx
import wx.lib.newevent as ne
import vtkmodules.util.numpy_support as vtk_np
import vtkmodules.vtkInteractionStyle # Ensure interactor styles are registered
from vtkmodules.vtkCommonCore import (
VTK_FLOAT,
VTK_UNSIGNED_CHAR,
vtkCommand,
vtkUnsignedCharArray,
vtkLookupTable
)
from vtkmodules.vtkCommonDataModel import vtkImageData, vtkPiecewiseFunction
from vtkmodules.vtkFiltersCore import vtkContourFilter
from vtkmodules.vtkFiltersSources import vtkCubeSource
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkColorTransferFunction,
vtkPolyDataMapper,
vtkRenderWindow,
vtkRenderer,
vtkVolume,
vtkVolumeProperty,
vtkWindowToImageFilter,
vtkTextActor
)
from vtkmodules.vtkIOImage import vtkPNGWriter
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor, vtkScalarBarActor
from vtkmodules.vtkInteractionWidgets import (
vtkOrientationMarkerWidget,
vtkImplicitPlaneWidget2,
vtkImplicitPlaneRepresentation
)
from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
import pyvcad as pv
from .color_palettes import resolve_color_map
# --------------------------
# wx Custom Events (binders)
# --------------------------
BuildStartedEvent, EVT_BUILD_STARTED = ne.NewEvent()
BuildFinishedEvent, EVT_BUILD_FINISHED = ne.NewEvent()
BuildFailedEvent, EVT_BUILD_FAILED = ne.NewEvent()
IsoSampleEvent, EVT_ISO_SAMPLE = ne.NewEvent()
IsoContourEvent, EVT_ISO_CONTOUR = ne.NewEvent()
IsoColorEvent, EVT_ISO_COLOR = ne.NewEvent()
VolSampleEvent, EVT_VOL_SAMPLE = ne.NewEvent()
[docs]
class VTKRenderPipeline:
[docs]
def __init__(self, event_target: wx.Window, use_default_interaction=True, render_window=None):
self.event_target = event_target
# Misc
self.optional_settings = None
# Rendering resources
self.renderer = vtkRenderer()
self.render_window = render_window or vtkRenderWindow()
self.render_window.AddRenderer(self.renderer)
# Interactor style (GUI thread only)
if use_default_interaction:
interactor = self.render_window.GetInteractor()
if interactor is not None:
style = vtkInteractorStyleTrackballCamera()
interactor.SetInteractorStyle(style)
self.corner_axis_widget = self._create_corner_axis_widget()
# Render state
self.render_mode = "iso_surface"
self.quality_profile = "low"
self.use_orthogonal_projection = False
self.use_volume_shading = False
self.use_volume_blending = True
self.attribute = "none"
self.scale_bar_palette_override = "auto"
self.active_scale_bar_palette = "viridis"
self.color_map, self.active_scale_bar_palette = resolve_color_map(self.attribute, self.scale_bar_palette_override)
self._show_bounding_box = False
self.bbox_actor = None
self.min_pt_text = None
self.max_pt_text = None
self._show_origin = False
self.origin_actor = None
self.tree_sampler = None
self.vcad_object = None
self.materials = None
self.voxel_size = None
self.current_volume = None
self.current_iso_surface = None
self.show_undefined_attribute_pattern = True
self.clipping_plane_enabled = False
self.clipping_plane_widget = None
self.clipping_plane_rep = None
self.clipping_plane_origin = None
self.clipping_plane_normal = None
self._first_place_done = False
# Async build state
self._gen = 0
self._pending_reset_camera = False
self._worker_thread = None
self._worker_lock = threading.Lock()
# Default progress callbacks -> post wx events
self.iso_surface_sample_progress_callback = lambda p: self._post(IsoSampleEvent, progress=float(p))
self.iso_surface_contouring_progress_callback = lambda p: self._post(IsoContourEvent, progress=float(p))
self.iso_surface_coloring_progress_callback = lambda p: self._post(IsoColorEvent, progress=float(p))
self.volume_sample_progress_callback = lambda p: self._post(VolSampleEvent, progress=float(p))
self.scalar_lut = vtkLookupTable()
self.scalar_lut.SetRange(0.0, 10.0)
self.scalar_lut.SetNumberOfTableValues(256)
self.scalar_lut.Build()
self._rebuild_scalar_lut_colors()
self.scalar_bar_actor = vtkScalarBarActor()
self.scalar_bar_actor.SetLookupTable(self.scalar_lut)
self.scalar_bar_actor.SetNumberOfLabels(5)
self.scalar_bar_actor.SetTitle("")
self.scalar_bar_actor.SetPosition(0.05, 0.05)
self.scalar_bar_actor.SetWidth(0.90)
self.scalar_bar_actor.SetHeight(0.08)
self.scalar_bar_actor.SetOrientationToHorizontal()
[docs]
def update_vcad_object(self, vcad_object, attribute="none", reset_camera=True, optional_settings=None, materials=None):
# Clear current view props immediately (GUI thread)
self.renderer.RemoveAllViewProps()
self.bbox_actor = None
self.current_iso_surface = None
self.current_volume = None
self.vcad_object = vcad_object
self.optional_settings = optional_settings
if materials is not None:
self.materials = materials
elif not hasattr(self, 'materials') or self.materials is None:
self.materials = pv.default_materials
self._pending_reset_camera = bool(reset_camera)
self.attribute = attribute
self._update_active_color_map()
if self.vcad_object is None:
self.render_window.Render()
return
# Bump generation to invalidate prior builds
with self._worker_lock:
self._gen += 1
gen = self._gen
# If we have optional settings, apply them
if self.optional_settings is not None:
self.render_mode = self.optional_settings['render_mode']
self.quality_profile = self.optional_settings['quality']
self.use_volume_shading = self.optional_settings['use_volume_shading']
self._show_bounding_box = self.optional_settings['show_bbox']
self._show_origin = self.optional_settings['show_origin']
self.use_volume_blending = self.optional_settings['use_blending']
self.clipping_plane_enabled = self.optional_settings.get('clipping_plane', False)
self.scale_bar_palette_override = self.optional_settings.get("scale_bar_palette", self.scale_bar_palette_override)
self.show_undefined_attribute_pattern = self.optional_settings.get('show_undefined_attribute_pattern', self.show_undefined_attribute_pattern)
self._update_active_color_map()
if self.clipping_plane_widget is None:
self.clipping_plane_widget, self.clipping_plane_rep = self._create_clipping_plane_widget()
min_pt, max_pt = self.vcad_object.bounding_box()
bounds = (min_pt.x, max_pt.x, min_pt.y, max_pt.y, min_pt.z, max_pt.z)
if not self._first_place_done:
cx = (min_pt.x + max_pt.x) / 2.0
cy = (min_pt.y + max_pt.y) / 2.0
cz = (min_pt.z + max_pt.z) / 2.0
self.clipping_plane_rep.PlaceWidget(bounds)
self.clipping_plane_rep.SetOrigin(cx, cy, cz)
self.clipping_plane_origin = self.clipping_plane_rep.GetOrigin()
self.clipping_plane_normal = self.clipping_plane_rep.GetNormal()
self._first_place_done = True
if self.clipping_plane_enabled:
self.clipping_plane_widget.On()
else:
self.clipping_plane_widget.Off()
cfg = dict(
render_mode=self.render_mode,
quality_profile=self.quality_profile,
use_volume_shading=self.use_volume_shading,
show_bbox=self._show_bounding_box,
show_origin=self._show_origin,
clipping_plane_enabled=self.clipping_plane_enabled,
clipping_plane_origin=self.clipping_plane_origin,
clipping_plane_normal=self.clipping_plane_normal,
show_undefined_attribute_pattern=self.show_undefined_attribute_pattern,
)
# Start worker thread
self._start_worker(gen, self.vcad_object, self.materials, self.scalar_bar_actor, cfg)
self._post(BuildStartedEvent, generation=gen)
[docs]
def update_selected_attribute(self, attribute):
self.attribute = attribute
self._update_active_color_map()
if self.vcad_object is None:
return
# Forcing a full rebuild is not strictly necessary here, we just need to update colors
# TODO: implement a lighter-weight update path
self.update_vcad_object(self.vcad_object, attribute, reset_camera=False, materials=self.materials)
[docs]
def set_progress_callbacks(
self,
iso_surface_sample=None,
iso_surface_contouring=None,
iso_surface_coloring=None,
volume_sample=None,
):
if iso_surface_sample is not None:
self.iso_surface_sample_progress_callback = iso_surface_sample
if iso_surface_contouring is not None:
self.iso_surface_contouring_progress_callback = iso_surface_contouring
if iso_surface_coloring is not None:
self.iso_surface_coloring_progress_callback = iso_surface_coloring
if volume_sample is not None:
self.volume_sample_progress_callback = volume_sample
[docs]
def get_render_mode(self):
return self.render_mode
[docs]
def show_bounding_box(self, show):
show = bool(show)
if show:
self.bbox_actor, self.min_pt_text, self.max_pt_text = self._create_bbox_actor(self.vcad_object)
self.renderer.AddActor(self.bbox_actor)
self.renderer.AddActor2D(self.min_pt_text)
self.renderer.AddActor2D(self.max_pt_text)
else:
self.renderer.RemoveActor(self.bbox_actor)
self.renderer.RemoveActor2D(self.min_pt_text)
self.renderer.RemoveActor2D(self.max_pt_text)
self.bbox_actor = None
self.min_pt_text = None
self.max_pt_text = None
self._show_bounding_box = show
self.render_window.Render()
[docs]
def show_origin(self, show):
show = bool(show)
if show:
self.origin_actor = self._create_origin_actor()
self.renderer.AddActor(self.origin_actor)
else:
self.renderer.RemoveActor(self.origin_actor)
self.origin_actor = None
self._show_origin = show
self.render_window.Render()
[docs]
def enable_orthogonal_projection(self, enable):
self.renderer.GetActiveCamera().SetParallelProjection(enable)
self.render_window.Render()
[docs]
def enable_volume_shading(self, enable):
if self.render_mode == "volumetric":
self.use_volume_shading = enable
if self.current_volume is not None:
if enable:
self.current_volume.GetProperty().ShadeOn()
else:
self.current_volume.GetProperty().ShadeOff()
self.render_window.Render()
[docs]
def enable_clipping_plane(self, enable: bool):
self.clipping_plane_enabled = enable
if self.vcad_object is not None:
if enable and self.clipping_plane_widget:
self.clipping_plane_widget.On()
elif self.clipping_plane_widget:
self.clipping_plane_widget.Off()
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
[docs]
def reset_clipping_plane(self):
if self.vcad_object is None or self.clipping_plane_rep is None: return
min_pt, max_pt = self.vcad_object.bounding_box()
bounds = (min_pt.x, max_pt.x, min_pt.y, max_pt.y, min_pt.z, max_pt.z)
cx = (min_pt.x + max_pt.x) / 2.0
cy = (min_pt.y + max_pt.y) / 2.0
cz = (min_pt.z + max_pt.z) / 2.0
self.clipping_plane_rep.PlaceWidget(bounds)
self.clipping_plane_rep.SetOrigin(cx, cy, cz)
self.clipping_plane_rep.SetNormal(1, 0, 0)
self.clipping_plane_origin = self.clipping_plane_rep.GetOrigin()
self.clipping_plane_normal = self.clipping_plane_rep.GetNormal()
if self.clipping_plane_enabled:
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
[docs]
def reset_camera(self):
min, max = self.vcad_object.bounding_box()
center = pv.Vec3(
(min.x + max.x) / 2, (min.y + max.y) / 2, (min.z + max.z) / 2
)
size = pv.Vec3(max.x - min.x, max.y - min.y, max.z - min.z)
distance = ((size.x**2) + (size.y**2) + (size.z**2)) ** 0.5 * 1.5
offset = distance / 3**0.5
cam = self.renderer.GetActiveCamera()
cam.SetPosition(center.x + offset, center.y + offset, center.z + offset)
cam.SetFocalPoint(center.x, center.y, center.z)
cam.SetViewUp(0, 0, 1)
self.renderer.ResetCamera()
self.renderer.ResetCameraClippingRange()
self.render_window.Render()
[docs]
def set_top_view(self):
cam = self.renderer.GetActiveCamera()
cam.SetPosition(0, 0, 1)
cam.SetViewUp(0, -1, 0)
cam.SetFocalPoint(0, 0, 0)
self.renderer.ResetCamera()
self.render_window.Render()
[docs]
def set_side_view(self):
cam = self.renderer.GetActiveCamera()
cam.SetPosition(1, 0, 0)
cam.SetViewUp(0, 0, 1)
cam.SetFocalPoint(0, 0, 0)
self.renderer.ResetCamera()
self.render_window.Render()
[docs]
def set_bottom_view(self):
cam = self.renderer.GetActiveCamera()
cam.SetPosition(0, 0, -1)
cam.SetViewUp(0, -1, 0)
cam.SetFocalPoint(0, 0, 0)
self.renderer.ResetCamera()
self.render_window.Render()
[docs]
def set_corner_view(self):
cam = self.renderer.GetActiveCamera()
cam.SetPosition(1, 1, 1)
cam.SetViewUp(0, 0, 1)
cam.SetFocalPoint(0, 0, 0)
self.renderer.ResetCamera()
self.render_window.Render()
[docs]
def set_render_mode(self, mode: str):
if mode not in ("volumetric", "iso_surface"):
raise ValueError(f"Invalid render mode: {mode}")
if mode == self.render_mode:
return
self.render_mode = mode
if self.vcad_object is None:
return
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
[docs]
def set_quality_profile(self, profile: str):
if profile not in ("low", "medium", "high", "ultra"):
raise ValueError(f"Invalid quality profile: {profile}")
if profile == self.quality_profile:
return
self.quality_profile = profile
if self.vcad_object is None:
return
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
[docs]
def set_scale_bar_palette(self, palette_name):
self.scale_bar_palette_override = palette_name
self._update_active_color_map()
if self.vcad_object is None:
self.render_window.Render()
return
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
[docs]
def take_screenshot(self, path: str):
self.corner_axis_widget.SetEnabled(False)
origin_was_visible = self._show_origin
if origin_was_visible and self.origin_actor:
self.renderer.RemoveActor(self.origin_actor)
self.render_window.Render()
bbox_was_visible = self._show_bounding_box
if bbox_was_visible and self.bbox_actor:
self.renderer.RemoveActor(self.bbox_actor)
self.renderer.RemoveActor2D(self.min_pt_text)
self.renderer.RemoveActor2D(self.max_pt_text)
self.render_window.Render()
window_to_image_filter = vtkWindowToImageFilter()
window_to_image_filter.SetInput(self.render_window)
window_to_image_filter.SetScale(2)
window_to_image_filter.SetInputBufferTypeToRGBA()
window_to_image_filter.ReadFrontBufferOff()
window_to_image_filter.Update()
writer = vtkPNGWriter()
writer.SetFileName(path)
writer.SetInputConnection(window_to_image_filter.GetOutputPort())
writer.SetFileDimensionality(2)
writer.Write()
self.corner_axis_widget.SetEnabled(False)
if origin_was_visible and self.origin_actor:
self.renderer.AddActor(self.origin_actor)
self.render_window.Render()
if bbox_was_visible and self.bbox_actor:
self.renderer.AddActor(self.bbox_actor)
self.renderer.AddActor2D(self.min_pt_text)
self.renderer.AddActor2D(self.max_pt_text)
self.render_window.Render()
[docs]
def set_background_color(self, r: float, g: float, b: float):
self.renderer.SetBackground(r, g, b)
[docs]
def enable_blending(self, enable: bool):
self.use_volume_blending = enable
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials) # Full rebuild required
[docs]
def set_undefined_attribute_pattern(self, enable: bool):
self.show_undefined_attribute_pattern = enable
if self.vcad_object is not None:
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
def _rebuild_scalar_lut_colors(self):
for i in range(256):
t = i / 255.0
color = self.color_map.get_color(t)
self.scalar_lut.SetTableValue(i, color.x, color.y, color.z, 255.0)
def _update_active_color_map(self):
self.color_map, self.active_scale_bar_palette = resolve_color_map(self.attribute, self.scale_bar_palette_override)
self._rebuild_scalar_lut_colors()
def _start_worker(self, gen, vcad_object, materials, scalar_bar_actor, cfg):
worker = _BuildWorker(self, gen, vcad_object, materials, scalar_bar_actor, cfg)
t = threading.Thread(target=worker.run, name=f"VTKBuild-{gen}", daemon=True)
self._worker_thread = t
t.start()
def _post(self, EventClass, **kwargs):
evt = EventClass(**kwargs)
wx.PostEvent(self.event_target, evt)
def _apply_payload(self, payload: dict):
widget_was_enabled = False
if self.clipping_plane_widget:
widget_was_enabled = self.clipping_plane_widget.GetEnabled()
if widget_was_enabled:
self.clipping_plane_widget.Off()
# Reset current props
self.renderer.RemoveAllViewProps()
self.current_iso_surface = None
self.current_volume = None
self.bbox_actor = None
self.min_pt_text = None
self.max_pt_text = None
self.origin_actor = None
# Update voxel size
self.voxel_size = payload.get("voxel_size")
# Optional items
if payload.get("bbox") is not None:
self.bbox_actor = payload["bbox"]
self.renderer.AddActor(self.bbox_actor)
if payload.get("min_pt") is not None:
self.min_pt_text = payload["min_pt"]
self.renderer.AddActor2D(self.min_pt_text)
if payload.get("max_pt") is not None:
self.max_pt_text = payload["max_pt"]
self.renderer.AddActor2D(self.max_pt_text)
if payload.get("origin") is not None:
self.origin_actor = payload["origin"]
self.renderer.AddActor(self.origin_actor)
# Main renderable
if payload.get("mode") == "vol":
self.current_volume = payload["volume"]
if self.current_volume is not None:
self.renderer.AddVolume(self.current_volume)
else:
self.current_iso_surface = payload["actor"]
if self.current_iso_surface is not None:
self.renderer.AddActor(self.current_iso_surface)
is_double = self.attribute == "Signed Distance" or pv.DefaultAttributes.get_return_type_by_name(self.attribute) == pv.Attribute.ReturnType.dbl
if is_double and payload.get("scalar_bar_actor") is not None:
self.scalar_bar_actor = payload["scalar_bar_actor"]
self.scalar_bar_actor.GetLookupTable().SetRange(payload["dbl_min"], payload["dbl_max"])
self.scalar_bar_actor.SetTitle(self.attribute)
self.renderer.AddActor2D(self.scalar_bar_actor)
# Corner axis widget (needs interactor; GUI thread only)
if hasattr(self, 'corner_axis_widget') and self.corner_axis_widget:
self.corner_axis_widget.SetEnabled(0)
self.corner_axis_widget = self._create_corner_axis_widget()
if self.clipping_plane_widget and widget_was_enabled:
self.clipping_plane_widget.On()
self.render_window.Render()
if self._pending_reset_camera:
self.reset_camera()
self._pending_reset_camera = False
def _create_bbox_actor(self, vcad_object):
min_pt, max_pt = vcad_object.bounding_box()
cube = vtkCubeSource()
cube.SetBounds(min_pt.x, max_pt.x, min_pt.y, max_pt.y, min_pt.z, max_pt.z)
cube_mapper = vtkPolyDataMapper()
cube_mapper.SetInputConnection(cube.GetOutputPort())
cube_actor = vtkActor()
cube_actor.SetMapper(cube_mapper)
cube_actor.GetProperty().SetOpacity(0.1)
# Create text for min/max coordinates
min_text = f"Min: ({min_pt.x}, {min_pt.y}, {min_pt.z})"
max_text = f"Max: ({max_pt.x}, {max_pt.y}, {max_pt.z})"
min_pt_text = vtkTextActor()
min_pt_text.SetInput(min_text)
min_pt_text.SetPosition(10, 10)
min_pt_text.GetTextProperty().SetFontSize(24)
min_pt_text.GetTextProperty().SetColor(1.0, 1.0, 1.0)
max_pt_text = vtkTextActor()
max_pt_text.SetInput(max_text)
max_pt_text.SetPosition(10, 40)
max_pt_text.GetTextProperty().SetFontSize(24)
max_pt_text.GetTextProperty().SetColor(1.0, 1.0, 1.0)
return cube_actor, min_pt_text, max_pt_text
def _create_origin_actor(self):
axes = vtkAxesActor()
axes.SetTotalLength(10.0, 10.0, 10.0)
axes.SetShaftTypeToLine()
axes.SetNormalizedShaftLength(1.0, 1.0, 1.0)
axes.SetNormalizedTipLength(0.2, 0.2, 0.2)
axes.SetPosition(0.0, 0.0, 0.0)
return axes
def _create_corner_axis_widget(self):
axes = vtkAxesActor()
widget = vtkOrientationMarkerWidget()
widget.SetOrientationMarker(axes)
widget.SetInteractor(self.render_window.GetInteractor())
widget.SetViewport(0.8, 0.8, 1.0, 1.0)
widget.SetEnabled(1)
widget.InteractiveOff()
return widget
def _create_clipping_plane_widget(self):
rep = vtkImplicitPlaneRepresentation()
rep.SetPlaceFactor(1.0)
rep.SetNormal(1, 0, 0)
# Don't draw the outline box around the plane, it looks better without it
rep.OutlineTranslationOff()
# Don't draw the actual plane surface to prevent z-fighting with the clipped face
rep.DrawPlaneOff()
widget = vtkImplicitPlaneWidget2()
widget.SetRepresentation(rep)
widget.SetInteractor(self.render_window.GetInteractor())
def on_interaction(caller, event):
# If the user holds the Shift key, snap the normal to the nearest cardinal axis
interactor = self.render_window.GetInteractor()
if interactor and interactor.GetShiftKey():
n = np.array(rep.GetNormal())
axes = np.array([
[1, 0, 0], [-1, 0, 0],
[0, 1, 0], [0, -1, 0],
[0, 0, 1], [0, 0, -1]
])
dots = np.dot(axes, n)
best_axis = axes[np.argmax(dots)]
rep.SetNormal(best_axis[0], best_axis[1], best_axis[2])
def on_interaction_end(caller, event):
self.clipping_plane_origin = rep.GetOrigin()
self.clipping_plane_normal = rep.GetNormal()
self.update_vcad_object(self.vcad_object, attribute=self.attribute, reset_camera=False, materials=self.materials)
widget.AddObserver("InteractionEvent", on_interaction)
widget.AddObserver("EndInteractionEvent", on_interaction_end)
return widget, rep
class _BuildWorker:
def __init__(self, pipeline: VTKRenderPipeline, gen: int, vcad_object, materials, scalar_bar_actor, cfg: dict):
self.pipeline = pipeline
self.gen = gen
self.vcad_object = vcad_object
self.materials = materials
self.scalar_bar_actor = scalar_bar_actor
self.cfg = cfg
def run(self):
p = self.pipeline
try:
old_iso_sample = p.iso_surface_sample_progress_callback
old_iso_contour = p.iso_surface_contouring_progress_callback
old_iso_color = p.iso_surface_coloring_progress_callback
old_vol_sample = p.volume_sample_progress_callback
p.iso_surface_sample_progress_callback = lambda v: p._post(IsoSampleEvent, progress=float(v))
p.iso_surface_contouring_progress_callback = lambda v: p._post(IsoContourEvent, progress=float(v))
p.iso_surface_coloring_progress_callback = lambda v: p._post(IsoColorEvent, progress=float(v))
p.volume_sample_progress_callback = lambda v: p._post(VolSampleEvent, progress=float(v))
from .vtk_utils import compute_voxel_size_for_quality_profile, create_volume_from_object, create_iso_surface_from_object
# Compute heavy stuff (off-thread)
voxel_size = compute_voxel_size_for_quality_profile(
self.vcad_object, self.cfg["quality_profile"]
)
if self.cfg.get("show_bbox"):
bbox_actor, min_pt_text, max_pt_text = p._create_bbox_actor(self.vcad_object)
else:
bbox_actor = min_pt_text = max_pt_text = None
origin_actor = p._create_origin_actor() if self.cfg["show_origin"] else None
tree_sampler = pv.TreeSampler(self.vcad_object, voxel_size, self.materials)
tree_sampler.set_undefined_attribute_pattern_enabled(self.cfg.get("show_undefined_attribute_pattern", True))
if self.cfg["render_mode"] == "volumetric":
volume = create_volume_from_object(
self.vcad_object, self.materials, voxel_size, tree_sampler,
p.attribute, p.color_map, p.use_volume_blending, p.volume_sample_progress_callback,
self.cfg["clipping_plane_enabled"], self.cfg["clipping_plane_origin"], self.cfg["clipping_plane_normal"]
)
if self.cfg["use_volume_shading"] and volume is not None:
volume.GetProperty().ShadeOn()
dbl_min, dbl_max = tree_sampler.get_double_attribute_min_max()
payload = {
"mode": "vol",
"voxel_size": voxel_size,
"volume": volume,
"bbox": bbox_actor,
"min_pt": min_pt_text,
"max_pt": max_pt_text,
"origin": origin_actor,
"scalar_bar_actor": self.scalar_bar_actor,
"dbl_min": dbl_min,
"dbl_max": dbl_max,
}
else:
iso_actor = create_iso_surface_from_object(
self.vcad_object, self.materials, voxel_size, 0.0, tree_sampler,
p.attribute, p.color_map, p.use_volume_blending,
p.iso_surface_sample_progress_callback, p.iso_surface_contouring_progress_callback, p.iso_surface_coloring_progress_callback,
self.cfg["clipping_plane_enabled"], self.cfg["clipping_plane_origin"], self.cfg["clipping_plane_normal"]
)
dbl_min, dbl_max = tree_sampler.get_double_attribute_min_max()
payload = {
"mode": "iso",
"voxel_size": voxel_size,
"actor": iso_actor,
"bbox": bbox_actor,
"min_pt": min_pt_text,
"max_pt": max_pt_text,
"origin": origin_actor,
"scalar_bar_actor": self.scalar_bar_actor,
"dbl_min": dbl_min,
"dbl_max": dbl_max,
}
# Only the newest generation should apply
def finish_on_ui():
if self.gen != p._gen:
return
p._apply_payload(payload)
p._post(BuildFinishedEvent, generation=self.gen)
wx.CallAfter(finish_on_ui)
finally:
# Restore original callbacks
p.iso_surface_sample_progress_callback = old_iso_sample
p.iso_surface_contouring_progress_callback = old_iso_contour
p.iso_surface_coloring_progress_callback = old_iso_color
p.volume_sample_progress_callback = old_vol_sample