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()