Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ae425a0
Initial plan
Copilot Dec 16, 2025
fc7b8eb
feat: add debug manager and directory setting
Copilot Dec 16, 2025
dd79214
chore: refine debug handling and options
Copilot Dec 16, 2025
ff55f3f
chore: address review follow ups
Copilot Dec 16, 2025
351a96a
fix: convert unit_name_field to 'UNITNAME'
lachlangrose Dec 17, 2025
2ec3fcd
fix: updating unload to prevent error when missing dock widgets
lachlangrose Dec 17, 2025
24cc539
fix: rename unit_name_column to unit_name_field
lachlangrose Dec 17, 2025
56ceb77
fix: update stratigraphic column with calculated thicknesses
lachlangrose Dec 17, 2025
5fba7c8
fix: update stratigraphic unit to prevent missing widget error
lachlangrose Dec 17, 2025
4a0ba19
style: formatting
lachlangrose Dec 17, 2025
752f217
Merge branch 'dev-0.1.12' into copilot/add-structured-logging-feature
lachlangrose Dec 17, 2025
9148640
chore: log layer sources in debug params
Copilot Dec 17, 2025
91d94e6
feat: export layers and add offline runner script
Copilot Dec 17, 2025
e590b19
fix: give debug manager the logger
lachlangrose Dec 18, 2025
c487dec
fix: add generic exporter for m2l objects
lachlangrose Dec 18, 2025
c74aedc
fix: pass debug manager to api for exporting packages
lachlangrose Dec 18, 2025
118aecc
move export to debug manager
lachlangrose Jan 6, 2026
e74bcc7
adding rebuild with debounce when geometry properties are changed
lachlangrose Jan 7, 2026
75ff03e
call update feature when rebuild is required
lachlangrose Jan 8, 2026
7208085
adding progress bar when model update is required
lachlangrose Jan 8, 2026
440fd2e
keep track of source feature for meshes
lachlangrose Jan 8, 2026
3a22d1a
update feature in viewer when it changes
lachlangrose Jan 14, 2026
52cccd1
fix: debug mode changes logging
lachlangrose Jan 14, 2026
37e0319
adding copilot fixes
lachlangrose Jan 14, 2026
cc9a0de
Merge branch 'dev-0.1.12' into fault-feature-panel-upgrade
lachlangrose Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QObject, Qt, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QMenu,
QMessageBox,
QProgressDialog,
QPushButton,
QSplitter,
QTreeWidget,
Expand All @@ -9,26 +11,47 @@
QWidget,
)

from .feature_details_panel import (
FaultFeatureDetailsPanel,
FoldedFeatureDetailsPanel,
FoliationFeatureDetailsPanel,
StructuralFrameFeatureDetailsPanel,
)
from LoopStructural.modelling.features import FeatureType

# Import the AddFaultDialog
from .add_fault_dialog import AddFaultDialog
from .add_foliation_dialog import AddFoliationDialog
from .add_unconformity_dialog import AddUnconformityDialog
from .feature_details_panel import (
FaultFeatureDetailsPanel,
FoldedFeatureDetailsPanel,
FoliationFeatureDetailsPanel,
StructuralFrameFeatureDetailsPanel,
)


class GeologicalModelTab(QWidget):
def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager
self.model_manager.observers.append(self.update_feature_list)
# Register update observer using Observable API if available
if self.model_manager is not None:
try:
# listen for model-level updates
self._disp_model = self.model_manager.attach(
self.update_feature_list, 'model_updated'
)
# show progress when model updates start/finish (covers indirect calls)
self._disp_update_start = self.model_manager.attach(
lambda _obs, _ev, *a, **k: self._on_model_update_started(),
'model_update_started',
)
self._disp_update_finish = self.model_manager.attach(
lambda _obs, _ev, *a, **k: self._on_model_update_finished(),
'model_update_finished',
)
except Exception:
# fallback to legacy list
try:
self.model_manager.observers.append(self.update_feature_list)
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# If the model manager does not expose a legacy 'observers' list or
# another non-critical error occurs here, ignore it and continue
# without registering the legacy model-level observer.

Copilot uses AI. Check for mistakes.
pass
# Main layout
mainLayout = QVBoxLayout(self)

Expand Down Expand Up @@ -75,6 +98,10 @@ def __init__(self, parent=None, *, model_manager=None, data_manager=None):
# Connect feature selection to update details panel
self.featureList.itemClicked.connect(self.on_feature_selected)

# thread handle to keep worker alive while running
self._model_update_thread = None
self._model_update_worker = None

def show_add_feature_menu(self, *args):
menu = QMenu(self)
add_fault = menu.addAction("Add Fault")
Expand All @@ -89,6 +116,7 @@ def show_add_feature_menu(self, *args):
self.open_add_foliation_dialog()
elif action == add_unconformity:
self.open_add_unconformity_dialog()

def open_add_fault_dialog(self):
dialog = AddFaultDialog(self)
if dialog.exec_() == dialog.Accepted:
Expand All @@ -102,16 +130,95 @@ def open_add_foliation_dialog(self):
)
if dialog.exec_() == dialog.Accepted:
pass

def open_add_unconformity_dialog(self):
dialog = AddUnconformityDialog(
self, data_manager=self.data_manager, model_manager=self.model_manager
)
if dialog.exec_() == dialog.Accepted:
pass

def initialize_model(self):
self.model_manager.update_model()
# Run update_model in a background thread to avoid blocking the UI.
if not self.model_manager:
return

# create progress dialog (indeterminate)
progress = QProgressDialog("Updating geological model...", "Cancel", 0, 0, self)
progress.setWindowModality(Qt.ApplicationModal)
progress.setWindowTitle("Updating Model")
progress.setCancelButton(None)
progress.setMinimumDuration(0)
progress.show()

# worker and thread
thread = QThread(self)
worker = _ModelUpdateWorker(self.model_manager)
worker.moveToThread(thread)

# When thread starts run worker.run
thread.started.connect(worker.run)

# on worker finished, notify observers on main thread and cleanup
def _on_finished():
try:
# notify observers now on main thread
try:
self.model_manager.notify('model_updated')
except Exception:
for obs in getattr(self.model_manager, 'observers', []):
try:
obs()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
finally:
try:
progress.close()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
# cleanup worker/thread
try:
worker.deleteLater()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
try:
thread.quit()
thread.wait(2000)
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

def _on_error(tb):
try:
progress.close()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
try:
QMessageBox.critical(
self,
"Model update failed",
f"An error occurred while updating the model:\n{tb}",
)
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# Best-effort display of error dialog; ignore failures in QMessageBox.

Copilot uses AI. Check for mistakes.
pass
# ensure thread cleanup
try:
worker.deleteLater()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
try:
thread.quit()
thread.wait(2000)
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

worker.finished.connect(_on_finished)
worker.error.connect(_on_error)
thread.finished.connect(thread.deleteLater)
self._model_update_thread = thread
self._model_update_worker = worker
thread.start()

def update_feature_list(self):
def update_feature_list(self, *args, **kwargs):
self.featureList.clear() # Clear the feature list before populating it
for feature in self.model_manager.features():
if feature.name.startswith("__"):
Expand Down Expand Up @@ -153,6 +260,43 @@ def on_feature_selected(self, item):
splitter.widget(1).deleteLater() # Remove the existing widget
splitter.addWidget(self.featureDetailsPanel) # Add the new widget

def _on_model_update_started(self):
"""Show a non-blocking indeterminate progress dialog for model updates.

This method is invoked via the Observable notifications and ensures the
user sees that a background or foreground update is in progress.
"""
print("Model update started - showing progress dialog")
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print statement left in production code. This should use proper logging instead of print().

Copilot uses AI. Check for mistakes.
try:
if getattr(self, '_progress_dialog', None) is None:
self._progress_dialog = QProgressDialog(
"Updating geological model...", None, 0, 0, self
)
self._progress_dialog.setWindowTitle("Updating Model")
self._progress_dialog.setWindowModality(Qt.ApplicationModal)
self._progress_dialog.setCancelButton(None)
self._progress_dialog.setMinimumDuration(0)
self._progress_dialog.show()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

def _on_model_update_finished(self):
"""Close the progress dialog shown for model updates."""
print("Model update finished - closing progress dialog")
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print statement left in production code. This should use proper logging instead of print().

Copilot uses AI. Check for mistakes.
try:
if getattr(self, '_progress_dialog', None) is not None:
try:
self._progress_dialog.close()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
try:
self._progress_dialog.deleteLater()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
self._progress_dialog = None
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

def show_feature_context_menu(self, pos):
# Show context menu only for items
item = self.featureList.itemAt(pos)
Expand Down Expand Up @@ -197,10 +341,50 @@ def delete_feature(self, item):

# Notify observers to refresh UI
try:
for obs in getattr(self.model_manager, 'observers', []):
try:
obs()
except Exception:
pass
# Prefer notify API
try:
self.model_manager.notify('model_updated')
except Exception:
# fallback to legacy observers list
for obs in getattr(self.model_manager, 'observers', []):
try:
obs()
except Exception:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass
except Exception:
pass


class _ModelUpdateWorker(QObject):
"""Worker that runs model_manager.update_model in a background thread.

Emits finished when done and error with a string if an exception occurs.
"""

finished = pyqtSignal()
error = pyqtSignal(str)

def __init__(self, model_manager):
super().__init__()
self.model_manager = model_manager

@pyqtSlot()
def run(self):
try:
# perform the expensive update
# run update without notifying observers from the background thread
try:
self.model_manager.update_model(notify_observers=False)
except TypeError:
# fallback if update_model signature not available
self.model_manager.update_model()
except Exception as e:
try:
import traceback

tb = traceback.format_exc()
except Exception:
tb = str(e)
self.error.emit(tb)
finally:
self.finished.emit()
Loading