Skip to content

Commit 1fb1539

Browse files
Fault feature panel upgrade (#63)
* Initial plan * feat: add debug manager and directory setting Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * chore: refine debug handling and options Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * chore: address review follow ups Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * fix: convert unit_name_field to 'UNITNAME' * fix: updating unload to prevent error when missing dock widgets * fix: rename unit_name_column to unit_name_field * fix: update stratigraphic column with calculated thicknesses * fix: update stratigraphic unit to prevent missing widget error * style: formatting * chore: log layer sources in debug params Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * feat: export layers and add offline runner script Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> * fix: give debug manager the logger * fix: add generic exporter for m2l objects * fix: pass debug manager to api for exporting packages * move export to debug manager * adding rebuild with debounce when geometry properties are changed * call update feature when rebuild is required * adding progress bar when model update is required * keep track of source feature for meshes * update feature in viewer when it changes * fix: debug mode changes logging * adding copilot fixes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com>
1 parent 400434c commit 1fb1539

File tree

8 files changed

+911
-196
lines changed

8 files changed

+911
-196
lines changed

loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py

Lines changed: 220 additions & 117 deletions
Large diffs are not rendered by default.

loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py

Lines changed: 199 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from PyQt5.QtCore import Qt
1+
from PyQt5.QtCore import QObject, Qt, QThread, pyqtSignal, pyqtSlot
22
from PyQt5.QtWidgets import (
33
QMenu,
4+
QMessageBox,
5+
QProgressDialog,
46
QPushButton,
57
QSplitter,
68
QTreeWidget,
@@ -9,26 +11,47 @@
911
QWidget,
1012
)
1113

12-
from .feature_details_panel import (
13-
FaultFeatureDetailsPanel,
14-
FoldedFeatureDetailsPanel,
15-
FoliationFeatureDetailsPanel,
16-
StructuralFrameFeatureDetailsPanel,
17-
)
1814
from LoopStructural.modelling.features import FeatureType
1915

2016
# Import the AddFaultDialog
2117
from .add_fault_dialog import AddFaultDialog
2218
from .add_foliation_dialog import AddFoliationDialog
2319
from .add_unconformity_dialog import AddUnconformityDialog
20+
from .feature_details_panel import (
21+
FaultFeatureDetailsPanel,
22+
FoldedFeatureDetailsPanel,
23+
FoliationFeatureDetailsPanel,
24+
StructuralFrameFeatureDetailsPanel,
25+
)
2426

2527

2628
class GeologicalModelTab(QWidget):
2729
def __init__(self, parent=None, *, model_manager=None, data_manager=None):
2830
super().__init__(parent)
2931
self.model_manager = model_manager
3032
self.data_manager = data_manager
31-
self.model_manager.observers.append(self.update_feature_list)
33+
# Register update observer using Observable API if available
34+
if self.model_manager is not None:
35+
try:
36+
# listen for model-level updates
37+
self._disp_model = self.model_manager.attach(
38+
self.update_feature_list, 'model_updated'
39+
)
40+
# show progress when model updates start/finish (covers indirect calls)
41+
self._disp_update_start = self.model_manager.attach(
42+
lambda _obs, _ev, *a, **k: self._on_model_update_started(),
43+
'model_update_started',
44+
)
45+
self._disp_update_finish = self.model_manager.attach(
46+
lambda _obs, _ev, *a, **k: self._on_model_update_finished(),
47+
'model_update_finished',
48+
)
49+
except Exception:
50+
# fallback to legacy list
51+
try:
52+
self.model_manager.observers.append(self.update_feature_list)
53+
except Exception:
54+
pass
3255
# Main layout
3356
mainLayout = QVBoxLayout(self)
3457

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

101+
# thread handle to keep worker alive while running
102+
self._model_update_thread = None
103+
self._model_update_worker = None
104+
78105
def show_add_feature_menu(self, *args):
79106
menu = QMenu(self)
80107
add_fault = menu.addAction("Add Fault")
@@ -89,6 +116,7 @@ def show_add_feature_menu(self, *args):
89116
self.open_add_foliation_dialog()
90117
elif action == add_unconformity:
91118
self.open_add_unconformity_dialog()
119+
92120
def open_add_fault_dialog(self):
93121
dialog = AddFaultDialog(self)
94122
if dialog.exec_() == dialog.Accepted:
@@ -102,16 +130,95 @@ def open_add_foliation_dialog(self):
102130
)
103131
if dialog.exec_() == dialog.Accepted:
104132
pass
133+
105134
def open_add_unconformity_dialog(self):
106135
dialog = AddUnconformityDialog(
107136
self, data_manager=self.data_manager, model_manager=self.model_manager
108137
)
109138
if dialog.exec_() == dialog.Accepted:
110139
pass
140+
111141
def initialize_model(self):
112-
self.model_manager.update_model()
142+
# Run update_model in a background thread to avoid blocking the UI.
143+
if not self.model_manager:
144+
return
145+
146+
# create progress dialog (indeterminate)
147+
progress = QProgressDialog("Updating geological model...", "Cancel", 0, 0, self)
148+
progress.setWindowModality(Qt.ApplicationModal)
149+
progress.setWindowTitle("Updating Model")
150+
progress.setCancelButton(None)
151+
progress.setMinimumDuration(0)
152+
progress.show()
153+
154+
# worker and thread
155+
thread = QThread(self)
156+
worker = _ModelUpdateWorker(self.model_manager)
157+
worker.moveToThread(thread)
158+
159+
# When thread starts run worker.run
160+
thread.started.connect(worker.run)
161+
162+
# on worker finished, notify observers on main thread and cleanup
163+
def _on_finished():
164+
try:
165+
# notify observers now on main thread
166+
try:
167+
self.model_manager.notify('model_updated')
168+
except Exception:
169+
for obs in getattr(self.model_manager, 'observers', []):
170+
try:
171+
obs()
172+
except Exception:
173+
pass
174+
finally:
175+
try:
176+
progress.close()
177+
except Exception:
178+
pass
179+
# cleanup worker/thread
180+
try:
181+
worker.deleteLater()
182+
except Exception:
183+
pass
184+
try:
185+
thread.quit()
186+
thread.wait(2000)
187+
except Exception:
188+
pass
189+
190+
def _on_error(tb):
191+
try:
192+
progress.close()
193+
except Exception:
194+
pass
195+
try:
196+
QMessageBox.critical(
197+
self,
198+
"Model update failed",
199+
f"An error occurred while updating the model:\n{tb}",
200+
)
201+
except Exception:
202+
pass
203+
# ensure thread cleanup
204+
try:
205+
worker.deleteLater()
206+
except Exception:
207+
pass
208+
try:
209+
thread.quit()
210+
thread.wait(2000)
211+
except Exception:
212+
pass
213+
214+
worker.finished.connect(_on_finished)
215+
worker.error.connect(_on_error)
216+
thread.finished.connect(thread.deleteLater)
217+
self._model_update_thread = thread
218+
self._model_update_worker = worker
219+
thread.start()
113220

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

263+
def _on_model_update_started(self):
264+
"""Show a non-blocking indeterminate progress dialog for model updates.
265+
266+
This method is invoked via the Observable notifications and ensures the
267+
user sees that a background or foreground update is in progress.
268+
"""
269+
print("Model update started - showing progress dialog")
270+
try:
271+
if getattr(self, '_progress_dialog', None) is None:
272+
self._progress_dialog = QProgressDialog(
273+
"Updating geological model...", None, 0, 0, self
274+
)
275+
self._progress_dialog.setWindowTitle("Updating Model")
276+
self._progress_dialog.setWindowModality(Qt.ApplicationModal)
277+
self._progress_dialog.setCancelButton(None)
278+
self._progress_dialog.setMinimumDuration(0)
279+
self._progress_dialog.show()
280+
except Exception:
281+
pass
282+
283+
def _on_model_update_finished(self):
284+
"""Close the progress dialog shown for model updates."""
285+
print("Model update finished - closing progress dialog")
286+
try:
287+
if getattr(self, '_progress_dialog', None) is not None:
288+
try:
289+
self._progress_dialog.close()
290+
except Exception:
291+
pass
292+
try:
293+
self._progress_dialog.deleteLater()
294+
except Exception:
295+
pass
296+
self._progress_dialog = None
297+
except Exception:
298+
pass
299+
156300
def show_feature_context_menu(self, pos):
157301
# Show context menu only for items
158302
item = self.featureList.itemAt(pos)
@@ -197,10 +341,50 @@ def delete_feature(self, item):
197341

198342
# Notify observers to refresh UI
199343
try:
200-
for obs in getattr(self.model_manager, 'observers', []):
201-
try:
202-
obs()
203-
except Exception:
204-
pass
344+
# Prefer notify API
345+
try:
346+
self.model_manager.notify('model_updated')
347+
except Exception:
348+
# fallback to legacy observers list
349+
for obs in getattr(self.model_manager, 'observers', []):
350+
try:
351+
obs()
352+
except Exception:
353+
pass
205354
except Exception:
206355
pass
356+
357+
358+
class _ModelUpdateWorker(QObject):
359+
"""Worker that runs model_manager.update_model in a background thread.
360+
361+
Emits finished when done and error with a string if an exception occurs.
362+
"""
363+
364+
finished = pyqtSignal()
365+
error = pyqtSignal(str)
366+
367+
def __init__(self, model_manager):
368+
super().__init__()
369+
self.model_manager = model_manager
370+
371+
@pyqtSlot()
372+
def run(self):
373+
try:
374+
# perform the expensive update
375+
# run update without notifying observers from the background thread
376+
try:
377+
self.model_manager.update_model(notify_observers=False)
378+
except TypeError:
379+
# fallback if update_model signature not available
380+
self.model_manager.update_model()
381+
except Exception as e:
382+
try:
383+
import traceback
384+
385+
tb = traceback.format_exc()
386+
except Exception:
387+
tb = str(e)
388+
self.error.emit(tb)
389+
finally:
390+
self.finished.emit()

0 commit comments

Comments
 (0)