From 369877ff4817b998efd40ecbc5947d2ad01258ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:18:34 +0000 Subject: [PATCH 1/3] Initial plan From 410bd6ca5c99b1ee225fa4697ac88c128679a304 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:24:58 +0000 Subject: [PATCH 2/3] Prevent model init with unset bounding box and persist flag Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../geological_model_tab.py | 8 ++++++ .../model_definition/bounding_box.py | 26 +++++++++++++++++++ loopstructural/main/data_manager.py | 19 +++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py b/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py index 287c25c..377a8d7 100644 --- a/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py +++ b/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py @@ -142,6 +142,14 @@ def initialize_model(self): # Run update_model in a background thread to avoid blocking the UI. if not self.model_manager: return + if self.data_manager is not None: + if not self.data_manager.is_bounding_box_set(): + QMessageBox.critical( + self, + "Bounding box required", + "Please set the bounding box before initializing the model.", + ) + return # create progress dialog (indeterminate) progress = QProgressDialog("Updating geological model...", "Cancel", 0, 0, self) diff --git a/loopstructural/gui/modelling/model_definition/bounding_box.py b/loopstructural/gui/modelling/model_definition/bounding_box.py index 6a32352..a611f8c 100644 --- a/loopstructural/gui/modelling/model_definition/bounding_box.py +++ b/loopstructural/gui/modelling/model_definition/bounding_box.py @@ -22,6 +22,7 @@ def __init__(self, parent=None, data_manager=None): self.useCurrentViewExtentButton.clicked.connect(self.useCurrentViewExtent) self.selectFromCurrentLayerButton.clicked.connect(self.selectFromCurrentLayer) self.data_manager.set_bounding_box_update_callback(self.set_bounding_box) + self._update_bounding_box_styles() def set_bounding_box(self, bounding_box): """Populate UI controls with values from a BoundingBox object. @@ -37,6 +38,7 @@ def set_bounding_box(self, bounding_box): self.maxYSpinBox.setValue(bounding_box.maximum[1]) self.originZSpinBox.setValue(bounding_box.origin[2]) self.maxZSpinBox.setValue(bounding_box.maximum[2]) + self._update_bounding_box_styles() def useCurrentViewExtent(self): """Set bounding box values from the current map canvas view extent.""" @@ -70,3 +72,27 @@ def selectFromCurrentLayer(self): def onChangeExtent(self, value): self.data_manager.set_bounding_box(**value) + try: + self._update_bounding_box_styles() + except Exception: + pass + + def _update_bounding_box_styles(self): + """Highlight spin boxes if bounding box has not been set.""" + if not hasattr(self, 'data_manager'): + return + try: + is_set = self.data_manager.is_bounding_box_set() + except Exception: + is_set = False + red_style = "border: 1px solid red;" + clear_style = "" + for sb in ( + self.originXSpinBox, + self.originYSpinBox, + self.originZSpinBox, + self.maxXSpinBox, + self.maxYSpinBox, + self.maxZSpinBox, + ): + sb.setStyleSheet(clear_style if is_set else red_style) diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 2143699..4c886c3 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -45,6 +45,7 @@ def __init__(self, *, project=None, mapCanvas=None, logger=None): default_bounding_box['zmax'], ], ) + self._bounding_box_set = False self._basal_contacts = None self._fault_traces = None @@ -99,7 +100,9 @@ def set_model_manager(self, model_manager): self._model_manager.set_fault_topology(self._fault_topology) self._model_manager.update_bounding_box(self._bounding_box) - def set_bounding_box(self, xmin=None, xmax=None, ymin=None, ymax=None, zmin=None, zmax=None): + def set_bounding_box( + self, xmin=None, xmax=None, ymin=None, ymax=None, zmin=None, zmax=None, *, mark_set=True + ): """Set the bounding box for the model.""" origin = self._bounding_box.origin maximum = self._bounding_box.maximum @@ -118,8 +121,8 @@ def set_bounding_box(self, xmin=None, xmax=None, ymin=None, ymax=None, zmin=None maximum[2] = zmax self._bounding_box.origin = origin self._bounding_box.maximum = maximum - self._bounding_box.origin = origin - self._bounding_box.maximum = maximum + if mark_set: + self._bounding_box_set = True self._model_manager.update_bounding_box(self._bounding_box) if self.bounding_box_callback: self.bounding_box_callback(self._bounding_box) @@ -128,6 +131,10 @@ def set_bounding_box_update_callback(self, callback): self.bounding_box_callback = callback self.bounding_box_callback(self._bounding_box) + def is_bounding_box_set(self): + """Return True if the bounding box has been explicitly set by the user.""" + return bool(self._bounding_box_set) + def set_fault_trace_layer_callback(self, callback): """Set the callback for when the fault trace layer is updated.""" self.fault_traces_callback = callback @@ -162,6 +169,7 @@ def get_bounding_box(self): """Get the current bounding box.""" return self._bounding_box + def set_elevation(self, elevation): """Set the elevation for the model.""" self.elevation = elevation @@ -457,6 +465,7 @@ def to_dict(self): return { 'bounding_box': self._bounding_box.to_dict(), + 'bounding_box_set': self._bounding_box_set, 'basal_contacts': basal_contacts, 'fault_traces': fault_traces, 'structural_orientations': structural_orientations, @@ -478,6 +487,7 @@ def from_dict(self, data): ymax=data['bounding_box']['maximum'][1], zmin=data['bounding_box']['origin'][2], zmax=data['bounding_box']['maximum'][2], + mark_set=data.get('bounding_box_set', True), ) if 'dem_layer' in data and data['dem_layer'] is not None: dem_layer = QgsProject.instance().mapLayersByName(data['dem_layer']) @@ -512,9 +522,10 @@ def update_from_dict(self, data): ymax=data['bounding_box']['maximum'][1], zmin=data['bounding_box']['origin'][2], zmax=data['bounding_box']['maximum'][2], + mark_set=data.get('bounding_box_set', True), ) else: - self.set_bounding_box(**default_bounding_box) + self.set_bounding_box(**default_bounding_box, mark_set=False) if 'dem_layer' in data and data['dem_layer'] is not None: dem_layer = QgsProject.instance().mapLayersByName(data['dem_layer']) if dem_layer: From 34557ad59710a52acee8eaee5dd84bbd4730ba8c Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Mon, 26 Jan 2026 18:16:34 +1030 Subject: [PATCH 3/3] fix: block spin boxes to prevent defaults triggering update --- .../model_definition/bounding_box.py | 36 +++++++++++++++---- loopstructural/main/data_manager.py | 1 - 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/loopstructural/gui/modelling/model_definition/bounding_box.py b/loopstructural/gui/modelling/model_definition/bounding_box.py index a611f8c..63a941c 100644 --- a/loopstructural/gui/modelling/model_definition/bounding_box.py +++ b/loopstructural/gui/modelling/model_definition/bounding_box.py @@ -32,12 +32,36 @@ def set_bounding_box(self, bounding_box): bounding_box : object BoundingBox-like object with `origin` and `maximum` sequences of length 3. """ - self.originXSpinBox.setValue(bounding_box.origin[0]) - self.maxXSpinBox.setValue(bounding_box.maximum[0]) - self.originYSpinBox.setValue(bounding_box.origin[1]) - self.maxYSpinBox.setValue(bounding_box.maximum[1]) - self.originZSpinBox.setValue(bounding_box.origin[2]) - self.maxZSpinBox.setValue(bounding_box.maximum[2]) + # Block spinbox signals to avoid emitting valueChanged while setting values + spinboxes = ( + self.originXSpinBox, + self.maxXSpinBox, + self.originYSpinBox, + self.maxYSpinBox, + self.originZSpinBox, + self.maxZSpinBox, + ) + for sb in spinboxes: + try: + sb.blockSignals(True) + except Exception: + pass + + try: + self.originXSpinBox.setValue(bounding_box.origin[0]) + self.maxXSpinBox.setValue(bounding_box.maximum[0]) + self.originYSpinBox.setValue(bounding_box.origin[1]) + self.maxYSpinBox.setValue(bounding_box.maximum[1]) + self.originZSpinBox.setValue(bounding_box.origin[2]) + self.maxZSpinBox.setValue(bounding_box.maximum[2]) + finally: + # Ensure signals are unblocked even if setting values raises + for sb in spinboxes: + try: + sb.blockSignals(False) + except Exception: + pass + self._update_bounding_box_styles() def useCurrentViewExtent(self): diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 4c886c3..d690b0f 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -169,7 +169,6 @@ def get_bounding_box(self): """Get the current bounding box.""" return self._bounding_box - def set_elevation(self, elevation): """Set the elevation for the model.""" self.elevation = elevation