Source code for pyvcad_rendering.vtk_pipeline_wx

# 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