Source code for pyvcad_rendering.render_frame

import wx
import wx.html
import datetime
import webbrowser
import json
import os

import pyvcad as pv
from fontTools.merge.util import first

from .vtk_pipeline_wx import (
    VTKRenderPipeline,
    EVT_BUILD_STARTED, EVT_BUILD_FINISHED, EVT_BUILD_FAILED,
    EVT_ISO_SAMPLE, EVT_ISO_CONTOUR, EVT_ISO_COLOR, EVT_VOL_SAMPLE
)
from .dpi_aware_interactor import DPIAwareVTKInteractor

from .vtk_export import export_iso_surface_vtk, export_volume_vtk
from .color_palettes import AVAILABLE_SCALE_BAR_PALETTES

def _is_dark_colour(col: wx.Colour) -> bool:
    """Fallback luminance check (sRGB) for older wx versions."""
    r, g, b = col.Red(), col.Green(), col.Blue()
    # relative luminance (approx.) -> threshold ~ 0.5
    lum = (0.2126 * (r/255.0)) + (0.7152 * (g/255.0)) + (0.0722 * (b/255.0))
    return lum < 0.5

[docs] class RenderFrame(wx.Frame):
[docs] def __init__(self, vcad_object, parent=None, title="OpenVCAD Renderer", state_file=None, initial_settings=None, materials=None): super().__init__(parent, title=title, size=(1000, 700)) self.progress_dialog = None self.vcad_object = vcad_object self.state_file = state_file self.current_settings = initial_settings if initial_settings else {} self.materials = materials # Track last progress value to enforce monotonic updates self._last_progress = 0 self._progress_tail = 1 # reserve tail slots for finalization self._finalizing_shown = False self._build_finished_early = False self._finalizing_timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self._on_finalizing_pulse, self._finalizing_timer) self.Bind(wx.EVT_CLOSE, self.on_close) # Create a panel in this frame and embed the VTK interactor panel = wx.Panel(self) panel_sizer = wx.BoxSizer(wx.VERTICAL) self.vtk_widget = DPIAwareVTKInteractor(panel, -1) panel_sizer.Add(self.vtk_widget, 1, wx.EXPAND) panel.SetSizer(panel_sizer) panel.Layout() # Frame sizer to hold the panel (so VTK fills the window) frame_sizer = wx.BoxSizer(wx.VERTICAL) frame_sizer.Add(panel, 1, wx.EXPAND) self.SetSizer(frame_sizer) self.Layout() self.vtk_widget.Enable(1) self.render_pipeline = VTKRenderPipeline( event_target=self, use_default_interaction=True, render_window=self.vtk_widget.GetRenderWindow(), ) self.Bind(EVT_BUILD_STARTED, lambda e: self.on_build_started(e.generation)) self.Bind(EVT_BUILD_FINISHED, lambda e: self.on_build_finished(e.generation)) self.Bind(EVT_BUILD_FAILED, lambda e: self.on_build_failed(getattr(e, "message", "Unknown error"))) self.Bind(EVT_ISO_SAMPLE, lambda e: self.on_iso_sample_progress(int(e.progress))) self.Bind(EVT_ISO_CONTOUR, lambda e: self.on_iso_contour_progress(int(e.progress))) self.Bind(EVT_ISO_COLOR, lambda e: self.on_iso_color_progress(int(e.progress))) self.Bind(EVT_VOL_SAMPLE, lambda e: self.on_vol_sample_progress(int(e.progress))) # ----- Menu Bar ----- menubar = wx.MenuBar() # ===== File ===== file_menu = wx.Menu() self.item_screenshot = file_menu.Append(wx.ID_ANY, "Screenshot") self.item_export_iso = file_menu.Append(wx.ID_ANY, "Export Iso-Surface (VTK)...") self.item_export_vol = file_menu.Append(wx.ID_ANY, "Export Volume (VTK)...") menubar.Append(file_menu, "&File") # ===== View ===== view_menu = wx.Menu() # Quality sub-menu (radio) self.quality_menu = wx.Menu() self.item_quality_low = self.quality_menu.AppendRadioItem(wx.ID_ANY, "Low") self.item_quality_medium = self.quality_menu.AppendRadioItem(wx.ID_ANY, "Medium") self.item_quality_high = self.quality_menu.AppendRadioItem(wx.ID_ANY, "High") self.item_quality_ultra = self.quality_menu.AppendRadioItem(wx.ID_ANY, "Ultra") # Load quality from settings or default init_quality = self.current_settings.get("quality", "low") if init_quality == "medium": self.item_quality_medium.Check(True) elif init_quality == "high": self.item_quality_high.Check(True) elif init_quality == "ultra": self.item_quality_ultra.Check(True) else: self.item_quality_low.Check(True) view_menu.AppendSubMenu(self.quality_menu, "Quality") view_menu.AppendSeparator() self.item_show_origin = view_menu.AppendCheckItem(wx.ID_ANY, "Show Origin") self.item_ortho_proj = view_menu.AppendCheckItem(wx.ID_ANY, "Orthographic Projection") self.item_dark_mode = view_menu.AppendCheckItem(wx.ID_ANY, "Dark Mode") view_menu.AppendSeparator() self.item_clipping_plane = view_menu.AppendCheckItem(wx.ID_ANY, "Enable Clipping Plane") self.item_reset_clipping_plane = view_menu.Append(wx.ID_ANY, "Reset Clipping Plane") if self.current_settings.get("show_origin", False): self.item_show_origin.Check(True) is_clip_enabled = self.current_settings.get("clipping_plane", False) self.item_clipping_plane.Check(is_clip_enabled) self.item_reset_clipping_plane.Enable(is_clip_enabled) view_menu.AppendSeparator() self.item_reset_camera = view_menu.Append(wx.ID_ANY, "Reset Camera") view_menu.AppendSeparator() self.item_top_view = view_menu.Append(wx.ID_ANY, "Top View") self.item_bottom_view = view_menu.Append(wx.ID_ANY, "Bottom View") self.item_side_view = view_menu.Append(wx.ID_ANY, "Side View") self.item_corner_view = view_menu.Append(wx.ID_ANY, "Corner View") menubar.Append(view_menu, "&View") # ===== Object ===== object_menu = wx.Menu() # Attribute menu self.attribute_menu = wx.Menu() design_attributes = vcad_object.attribute_list() auto_attributes = ["none", "Signed Distance"] attributes = auto_attributes + design_attributes target_attr = self.current_settings.get("visualized_attribute", None) # If target attr not in list, default to first if target_attr not in attributes: target_attr = attributes[0] if attributes else None for attr in attributes: new_item = self.attribute_menu.AppendCheckItem(wx.ID_ANY, f"{attr}") self.Bind(wx.EVT_MENU, self._on_visualized_attribute_any, new_item) if attr == target_attr: new_item.Check(True) # Add separator between auto and design attributes if attr == "Signed Distance" and len(design_attributes) > 0: self.attribute_menu.AppendSeparator() object_menu.AppendSubMenu(self.attribute_menu, "Visualized Attribute") self.scale_bar_palette_menu = wx.Menu() self.scale_bar_palette_items = {} all_palette_options = ("auto",) + tuple(AVAILABLE_SCALE_BAR_PALETTES) init_scale_bar_palette = self.current_settings.get("scale_bar_palette", "auto") if init_scale_bar_palette not in all_palette_options: init_scale_bar_palette = "auto" for palette_name in all_palette_options: label = "Auto (per attribute)" if palette_name == "auto" else palette_name.title() item = self.scale_bar_palette_menu.AppendRadioItem(wx.ID_ANY, label) self.scale_bar_palette_items[palette_name] = item self.Bind(wx.EVT_MENU, self._on_scale_bar_palette_any, item) if palette_name == init_scale_bar_palette: item.Check(True) self.item_scale_bar_palette_menu = object_menu.AppendSubMenu(self.scale_bar_palette_menu, "Scale Bar Palette") # Mode sub-menu (radio) self.render_mode_menu = wx.Menu() self.item_iso_surface = self.render_mode_menu.AppendRadioItem(wx.ID_ANY, "Iso-surface") self.item_volumetric = self.render_mode_menu.AppendRadioItem(wx.ID_ANY, "Volumetric") init_mode = self.current_settings.get("render_mode", "iso_surface") if init_mode == "volumetric": self.item_volumetric.Check(True) else: self.item_iso_surface.Check(True) object_menu.AppendSubMenu(self.render_mode_menu, "Mode") self.item_show_bbox = object_menu.AppendCheckItem(wx.ID_ANY, "Show bounding box") if self.current_settings.get("show_bbox", False): self.item_show_bbox.Check(True) self.item_blending = object_menu.AppendCheckItem(wx.ID_ANY, "Use Blending") if self.current_settings.get("use_blending", True): self.item_blending.Check(True) else: self.item_blending.Check(False) self.item_volumetric_shading = object_menu.AppendCheckItem(wx.ID_ANY, "Use Volumetric Shading") if self.current_settings.get("use_vol_shading", False): self.item_volumetric_shading.Check(True) object_menu.AppendSeparator() self.item_undefined_pattern = object_menu.AppendCheckItem(wx.ID_ANY, "Show Undefined Attribute Pattern") if self.current_settings.get("show_undefined_attribute_pattern", True): self.item_undefined_pattern.Check(True) menubar.Append(object_menu, "&Object") # ===== Help ===== help_menu = wx.Menu() self.item_about = help_menu.Append(wx.ID_ANY, "About") help_menu.AppendSeparator() self.item_wiki = help_menu.Append(wx.ID_ANY, "Wiki") self.item_docs = help_menu.Append(wx.ID_ANY, "Library Documentation") self.item_getting_started = help_menu.Append(wx.ID_ANY, "Getting Started") help_menu.AppendSeparator() self.item_report_bug = help_menu.Append(wx.ID_ANY, "Report a Bug") menubar.Append(help_menu, "&Help") self.SetMenuBar(menubar) # ----- Bind menu events ----- # File self.Bind(wx.EVT_MENU, self.on_screenshot, self.item_screenshot) self.Bind(wx.EVT_MENU, self.on_export_iso_surface, self.item_export_iso) self.Bind(wx.EVT_MENU, self.on_export_volume, self.item_export_vol) # View self.Bind(wx.EVT_MENU, self.on_toggle_show_origin, self.item_show_origin) self.Bind(wx.EVT_MENU, self.on_toggle_ortho_projection, self.item_ortho_proj) self.Bind(wx.EVT_MENU, self.on_toggle_dark_mode, self.item_dark_mode) self.Bind(wx.EVT_MENU, self.on_toggle_clipping_plane, self.item_clipping_plane) self.Bind(wx.EVT_MENU, self.on_reset_clipping_plane, self.item_reset_clipping_plane) self.Bind(wx.EVT_MENU, self.on_reset_camera, self.item_reset_camera) self.Bind(wx.EVT_MENU, self.on_set_top_view, self.item_top_view) self.Bind(wx.EVT_MENU, self.on_set_bottom_view, self.item_bottom_view) self.Bind(wx.EVT_MENU, self.on_set_side_view, self.item_side_view) self.Bind(wx.EVT_MENU, self.on_set_corner_view, self.item_corner_view) # Quality radio group → one handler, read which is checked self.Bind(wx.EVT_MENU, self._on_quality_any, self.item_quality_low) self.Bind(wx.EVT_MENU, self._on_quality_any, self.item_quality_medium) self.Bind(wx.EVT_MENU, self._on_quality_any, self.item_quality_high) self.Bind(wx.EVT_MENU, self._on_quality_any, self.item_quality_ultra) # Object → Mode radios self.Bind(wx.EVT_MENU, self._on_mode_any, self.item_iso_surface) self.Bind(wx.EVT_MENU, self._on_mode_any, self.item_volumetric) # Object toggles self.Bind(wx.EVT_MENU, self.on_toggle_show_bbox, self.item_show_bbox) self.Bind(wx.EVT_MENU, self.on_toggle_blending, self.item_blending) self.Bind(wx.EVT_MENU, self.on_toggle_volumetric_shading, self.item_volumetric_shading) self.Bind(wx.EVT_MENU, self.on_toggle_undefined_pattern, self.item_undefined_pattern) # Help self.Bind(wx.EVT_MENU, self.on_about, self.item_about) self.Bind(wx.EVT_MENU, self.on_wiki, self.item_wiki) self.Bind(wx.EVT_MENU, self.on_docs, self.item_docs) self.Bind(wx.EVT_MENU, self.on_getting_started, self.item_getting_started) self.Bind(wx.EVT_MENU, self.on_report_bug, self.item_report_bug) self.Centre() self.Show() # Update if the OS theme changes while the app is running self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self._on_sys_colour_changed) self.item_volumetric_shading.Enable(init_mode == "volumetric") # Determine initial dark mode state import sys is_dark_init = False if not sys.platform.startswith("win"): try: appearance = wx.SystemSettings.GetAppearance() is_dark_init = appearance.IsDark() except AttributeError: win_col = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) is_dark_init = _is_dark_colour(win_col) # Check for saved setting, otherwise use system default v = self.current_settings.get("dark_mode", is_dark_init) self.item_dark_mode.Check(v) self._apply_theme_colors(do_render=False) optional_settings = dict( render_mode=init_mode, quality=init_quality, show_origin=self.item_show_origin.IsChecked(), show_bbox=self.item_show_bbox.IsChecked(), use_blending=self.item_blending.IsChecked(), use_volume_shading=self.item_volumetric_shading.IsChecked(), clipping_plane=self.item_clipping_plane.IsChecked(), scale_bar_palette=init_scale_bar_palette, show_undefined_attribute_pattern=self.item_undefined_pattern.IsChecked() ) # Main Update start_attr = target_attr if target_attr in attributes else attributes[0] self._update_blending_menu_state(start_attr) self._update_scale_bar_palette_menu_state(start_attr) self.render_pipeline.update_vcad_object(self.vcad_object, start_attr, optional_settings=optional_settings, materials=self.materials)
[docs] def on_close(self, event): if self.state_file: # Gather current settings from UI/pipeline state # Render Mode mode = "volumetric" if self.item_volumetric.IsChecked() else "iso_surface" # Quality quality = "low" if self.item_quality_medium.IsChecked(): quality = "medium" if self.item_quality_high.IsChecked(): quality = "high" if self.item_quality_ultra.IsChecked(): quality = "ultra" # Attribute selected_attr = None for item in self.attribute_menu.GetMenuItems(): if item.IsChecked(): selected_attr = item.GetItemLabelText() break new_settings = { "render_mode": mode, "quality": quality, "show_origin": self.item_show_origin.IsChecked(), "show_bbox": self.item_show_bbox.IsChecked(), "use_blending": self.item_blending.IsChecked(), "use_vol_shading": self.item_volumetric_shading.IsChecked(), "visualized_attribute": selected_attr, "dark_mode": self.item_dark_mode.IsChecked(), "clipping_plane": self.item_clipping_plane.IsChecked(), "scale_bar_palette": self._get_selected_scale_bar_palette(), "show_undefined_attribute_pattern": self.item_undefined_pattern.IsChecked() } # Need to read existing file first to preserve the caller_id data = {} if os.path.exists(self.state_file): try: with open(self.state_file, 'r') as f: data = json.load(f) except: pass data['last_settings'] = new_settings # Note: caller_id is updated by the launcher, not here usually, # but we just update settings part. try: with open(self.state_file, 'w') as f: json.dump(data, f, indent=4) except Exception as e: print(f"Failed to save render state: {e}") self.Destroy()
[docs] def on_build_started(self, _=0): self._stop_finalizing_pulse() is_volumetric = self.render_pipeline.get_render_mode() == "volumetric" base_max = 100 if is_volumetric else 300 tail = max(1, getattr(self, "_progress_tail", 1)) self._progress_tail = tail maximum = base_max + tail # Reserve a slot so wx.PD_AUTO_HIDE waits for explicit finish # Reset monotonic tracker for a new build self._last_progress = 0 self._finalizing_shown = False self._build_finished_early = False # Max=300 for iso-surface (3 phases), 100 for volumetric (1 phase) self.progress_dialog = wx.ProgressDialog( "Rendering…", "Initializing…", maximum=maximum, parent=self, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE | wx.PD_CAN_ABORT ) # If the build finished while the dialog was being created (e.g. on Windows event loop pumping) if self._build_finished_early: self.progress_dialog.Destroy() self.progress_dialog = None return self.progress_dialog.Update(0, "Initializing…")
[docs] def on_build_finished(self, _=0): self._stop_finalizing_pulse() if self.progress_dialog: # Update to max value regardless of mode before destroying max_val = self.progress_dialog.GetRange() self.progress_dialog.Update(max_val, "Done") self.progress_dialog.Destroy() self.progress_dialog = None else: self._build_finished_early = True # Reset tracker after build completes self._last_progress = 0 self._finalizing_shown = False
[docs] def on_build_failed(self, msg: str): self._stop_finalizing_pulse() if self.progress_dialog: self.progress_dialog.Destroy() self.progress_dialog = None else: self._build_finished_early = True # Reset tracker on failure self._last_progress = 0 self._finalizing_shown = False wx.MessageBox(msg, "Build failed", wx.OK | wx.ICON_WARNING, parent=self)
# Monotonic update helper: only advance the bar, never regress def _update_progress(self, value: int, message: str): if not self.progress_dialog: return # Ignore if not advancing if value <= self._last_progress: return # Clamp to dialog range and update max_val = self.progress_dialog.GetRange() tail = max(1, getattr(self, "_progress_tail", 1)) capped_max = max_val - tail if max_val > tail else max_val value = min(value, capped_max) self._last_progress = value self.progress_dialog.Update(value, message) def _on_finalizing_pulse(self, _evt): if not self.progress_dialog: self._stop_finalizing_pulse() return cont, _ = self.progress_dialog.Pulse("Finalizing…") if not cont: self._stop_finalizing_pulse() def _stop_finalizing_pulse(self): if hasattr(self, '_finalizing_timer') and self._finalizing_timer.IsRunning(): self._finalizing_timer.Stop() def _show_finalizing_message(self): if not self.progress_dialog or self._finalizing_shown: return self._finalizing_shown = True max_val = self.progress_dialog.GetRange() tail = max(1, getattr(self, "_progress_tail", 1)) final_value = max_val - tail if max_val > tail else max_val final_value = max(self._last_progress, final_value) self._last_progress = final_value self.progress_dialog.Update(final_value, "Finalizing…") self.progress_dialog.Pulse("Finalizing…") self._finalizing_timer.Start(150)
[docs] def on_iso_sample_progress(self, p: float): if self.progress_dialog: self._update_progress(int(p), "Phase 1/3: Sampling…")
[docs] def on_iso_contour_progress(self, p: float): if self.progress_dialog: self._update_progress(100 + int(p), "Phase 2/3: Contouring…")
[docs] def on_iso_color_progress(self, p: float): if self.progress_dialog: self._update_progress(200 + int(p), "Phase 3/3: Coloring…") if p >= 100: self._show_finalizing_message()
[docs] def on_vol_sample_progress(self, p: float): if self.progress_dialog: # Volumetric mode is a single phase from 0-100 self._update_progress(int(p), "Volume Sampling…") if p >= 100: self._show_finalizing_message()
[docs] def on_screenshot(self, _evt=None): with wx.FileDialog( self, message="Save Screenshot", defaultFile="screenshot.png", wildcard="PNG Image (*.png)|*.png", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT ) as dlg: if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() self.render_pipeline.take_screenshot(path)
[docs] def on_export_iso_surface(self, _evt=None): with wx.FileDialog( self, message="Export Iso-Surface (VTK)", defaultFile="isosurface.vtp", wildcard="VTK PolyData (*.vtp)|*.vtp", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT ) as dlg: if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() dlg_prog = wx.ProgressDialog( "Exporting Iso-Surface", "Generating VTK data...", maximum=100, parent=self, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE ) def progress_cb(p): dlg_prog.Update(int(p), "Generating VTK data...") wx.GetApp().Yield() quality = self.current_settings.get("quality", "low") use_blending = self.current_settings.get("use_blending", True) try: export_iso_surface_vtk(self.vcad_object, path, quality=quality, use_blending=use_blending, progress_callback=progress_cb) except Exception as e: wx.MessageBox(f"Failed to export: {e}", "Export Error", wx.OK | wx.ICON_ERROR, parent=self) finally: dlg_prog.Destroy()
[docs] def on_export_volume(self, _evt=None): with wx.FileDialog( self, message="Export Volume (VTK)", defaultFile="volume.vti", wildcard="VTK ImageData (*.vti)|*.vti", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT ) as dlg: if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() dlg_prog = wx.ProgressDialog( "Exporting Volume", "Generating VTK data...", maximum=100, parent=self, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE ) def progress_cb(p): dlg_prog.Update(int(p), "Generating VTK data...") wx.GetApp().Yield() quality = self.current_settings.get("quality", "low") use_blending = self.current_settings.get("use_blending", True) try: export_volume_vtk(self.vcad_object, path, quality=quality, use_blending=use_blending, progress_callback=progress_cb) except Exception as e: wx.MessageBox(f"Failed to export: {e}", "Export Error", wx.OK | wx.ICON_ERROR, parent=self) finally: dlg_prog.Destroy()
[docs] def on_about(self, _evt=None): year = datetime.date.today().year try: import pyvcad as pv vcad_version = pv.version() except Exception: vcad_version = "unknown" dlg = wx.Dialog(self, title="About OpenVCAD Renderer") # Main sizer for the dialog content main_sizer = wx.BoxSizer(wx.VERTICAL) # Title title_font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) title_font.SetPointSize(title_font.GetPointSize() + 4) title_font.SetWeight(wx.FONTWEIGHT_BOLD) title_text = wx.StaticText(dlg, label="OpenVCAD Renderer") title_text.SetFont(title_font) main_sizer.Add(title_text, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 15) # Content sizer content_sizer = wx.BoxSizer(wx.VERTICAL) content_sizer.Add(wx.StaticText(dlg, label=f"Built by the Matter Assembly Computation Lab"), 0, wx.BOTTOM, 5) content_sizer.Add(wx.StaticText(dlg, label=f"Using pyvcad version: {vcad_version}"), 0, wx.BOTTOM, 5) content_sizer.Add(wx.StaticText(dlg, label=f"© Charles Wade and Robert MacCurdy {year}"), 0, wx.BOTTOM, 10) content_sizer.Add(wx.StaticLine(dlg), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) # Disclaimer disclaimer_font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) disclaimer_font.SetWeight(wx.FONTWEIGHT_BOLD) disclaimer_title = wx.StaticText(dlg, label="DISCLAIMER:") disclaimer_title.SetFont(disclaimer_font) content_sizer.Add(disclaimer_title, 0, wx.BOTTOM, 5) disclaimer_body = wx.StaticText(dlg, label=( "OpenVCAD Open Source is a research tool and is not permitted for commercial use.\n" "For commercial use, please contact Charles Wade at:\n" "charles.wade@colorado.edu" )) content_sizer.Add(disclaimer_body, 0, wx.BOTTOM, 15) main_sizer.Add(content_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 20) # OK Button main_sizer.Add(wx.Button(dlg, wx.ID_OK, "OK"), 0, wx.ALIGN_CENTER | wx.BOTTOM, 10) dlg.SetSizerAndFit(main_sizer) dlg.CentreOnParent() dlg.ShowModal() dlg.Destroy()
[docs] def on_wiki(self, _evt=None): webbrowser.open("https://github.com/MacCurdyLab/OpenVCAD-Public/wiki")
[docs] def on_docs(self, _evt=None): webbrowser.open("https://matterassembly.org/pyvcad")
[docs] def on_getting_started(self, _evt=None): webbrowser.open("https://github.com/MacCurdyLab/OpenVCAD-Public/wiki/Getting-Started-with-OpenVCAD")
[docs] def on_report_bug(self, _evt=None): webbrowser.open("https://github.com/MacCurdyLab/OpenVCAD-Public/issues/new?template=bug_report.md")
[docs] def on_reset_camera(self, _evt=None): self.render_pipeline.reset_camera()
[docs] def on_toggle_ortho_projection(self, _evt=None): checked = self.item_ortho_proj.IsChecked() self.render_pipeline.enable_orthogonal_projection(checked)
[docs] def on_toggle_clipping_plane(self, _evt=None): checked = self.item_clipping_plane.IsChecked() self.item_reset_clipping_plane.Enable(checked) self.render_pipeline.enable_clipping_plane(checked)
[docs] def on_reset_clipping_plane(self, _evt=None): self.render_pipeline.reset_clipping_plane()
[docs] def on_toggle_dark_mode(self, _evt=None): self._apply_theme_colors()
def _on_visualized_attribute_any(self, evt): id = evt.GetId() attribute_name = None # Find which attribute was selected based on the ID and uncheck others for item in self.attribute_menu.GetMenuItems(): if item.IsSeparator(): continue if item.GetId() == id: item.Check(True) attribute_name = item.GetItemLabelText() else: item.Check(False) if attribute_name is not None: self.on_visualized_attribute_changed(attribute_name)
[docs] def on_visualized_attribute_changed(self, attribute_name): self._update_blending_menu_state(attribute_name) self._update_scale_bar_palette_menu_state(attribute_name) self.render_pipeline.update_selected_attribute(attribute_name)
def _attribute_supports_blending(self, attribute_name): if attribute_name is None: return False attr_text = str(attribute_name).strip().lower() vf_attr_text = str(pv.DefaultAttributes.VOLUME_FRACTIONS).strip().lower() return attr_text == vf_attr_text def _update_blending_menu_state(self, attribute_name): self.item_blending.Enable(self._attribute_supports_blending(attribute_name)) def _attribute_supports_scalar_palette(self, attribute_name): if attribute_name == "Signed Distance": return True try: return pv.DefaultAttributes.get_return_type_by_name(attribute_name) == pv.Attribute.ReturnType.dbl except Exception: return False def _update_scale_bar_palette_menu_state(self, attribute_name): self.item_scale_bar_palette_menu.Enable(self._attribute_supports_scalar_palette(attribute_name)) def _on_scale_bar_palette_any(self, evt): selected_id = evt.GetId() selected_palette = "auto" for palette_name, item in self.scale_bar_palette_items.items(): if item.GetId() == selected_id: selected_palette = palette_name break self.render_pipeline.set_scale_bar_palette(selected_palette) def _get_selected_scale_bar_palette(self): for palette_name, item in self.scale_bar_palette_items.items(): if item.IsChecked(): return palette_name return "auto" def _on_quality_any(self, evt): quality_map = { # Map checked item -> quality string self.item_quality_low.GetId(): "low", self.item_quality_medium.GetId(): "medium", self.item_quality_high.GetId(): "high", self.item_quality_ultra.GetId(): "ultra", } quality = quality_map.get(evt.GetId(), "low") self.on_quality_changed(quality)
[docs] def on_quality_changed(self, quality: str): self.render_pipeline.set_quality_profile(quality)
[docs] def on_toggle_show_bbox(self, _evt=None): checked = self.item_show_bbox.IsChecked() self.render_pipeline.show_bounding_box(checked)
[docs] def on_toggle_show_origin(self, _evt=None): checked = self.item_show_origin.IsChecked() self.render_pipeline.show_origin(checked)
[docs] def on_set_top_view(self, _evt=None): self.render_pipeline.set_top_view()
[docs] def on_set_bottom_view(self, _evt=None): self.render_pipeline.set_bottom_view()
[docs] def on_set_side_view(self, _evt=None): self.render_pipeline.set_side_view()
[docs] def on_set_corner_view(self, _evt=None): self.render_pipeline.set_corner_view()
def _on_mode_any(self, evt): if evt.GetId() == self.item_volumetric.GetId(): self.on_render_mode_changed("volumetric") else: self.on_render_mode_changed("iso_surface")
[docs] def on_render_mode_changed(self, mode: str): self.render_pipeline.set_render_mode(mode) # Enable/disable volumetric submenu like Qt version is_vol = (mode == "volumetric") self.item_volumetric_shading.Enable(is_vol)
[docs] def on_toggle_blending(self, _evt=None): checked = self.item_blending.IsChecked() self.render_pipeline.enable_blending(checked)
[docs] def on_toggle_volumetric_shading(self, _evt=None): checked = self.item_volumetric_shading.IsChecked() self.render_pipeline.enable_volume_shading(checked)
[docs] def on_toggle_undefined_pattern(self, _evt=None): self.render_pipeline.set_undefined_attribute_pattern(self.item_undefined_pattern.IsChecked())
def _apply_theme_colors(self, do_render=True): """Set the VTK background based on the Dark Mode menu item.""" is_dark = self.item_dark_mode.IsChecked() if is_dark: # Use dark gray instead of pure black self.render_pipeline.set_background_color(0.177, 0.177, 0.177) else: self.render_pipeline.set_background_color(1.0, 1.0, 1.0) # Trigger a render to update the background immediately if do_render: self.vtk_widget.GetRenderWindow().Render() def _on_sys_colour_changed(self, _evt): # If the user has explicitly set a preference in the state, do not override it with system changes. if "dark_mode" in self.current_settings: return import sys # On Mac/Linux, respect system changes if no user override if not sys.platform.startswith("win"): try: appearance = wx.SystemSettings.GetAppearance() is_dark = appearance.IsDark() except AttributeError: win_col = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) is_dark = _is_dark_colour(win_col) # Update the toggle to match system change self.item_dark_mode.Check(is_dark) self._apply_theme_colors()