Skip to content

Commit 8647c0e

Browse files
authored
Merge pull request #43 from Loop3D/fix/exports
Export and visualisation improvements
2 parents a84df78 + c905dda commit 8647c0e

File tree

12 files changed

+2209
-144
lines changed

12 files changed

+2209
-144
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import numpy as np
2+
from PyQt5.QtCore import Qt
3+
from PyQt5.QtWidgets import (
4+
QGridLayout,
5+
QLabel,
6+
QDoubleSpinBox,
7+
QVBoxLayout,
8+
QWidget,
9+
)
10+
from qgis.gui import QgsCollapsibleGroupBox
11+
12+
from LoopStructural import getLogger
13+
logger = getLogger(__name__)
14+
15+
16+
class BoundingBoxWidget(QWidget):
17+
"""Standalone bounding-box widget used in the export/evaluation panel.
18+
19+
Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
20+
control for the overall element count (nelements). The widget keeps
21+
itself in sync with the authoritative bounding_box available from a
22+
provided data_manager or model_manager and will call back to update
23+
the model when the user changes values.
24+
"""
25+
26+
def __init__(self, parent=None, *, model_manager=None, data_manager=None):
27+
super().__init__(parent)
28+
self.model_manager = model_manager
29+
self.data_manager = data_manager
30+
31+
# Create the inner layout that will be placed inside a collapsible group widget
32+
inner_layout = QVBoxLayout()
33+
inner_layout.setContentsMargins(0, 0, 0, 0)
34+
inner_layout.setSpacing(6)
35+
36+
grid = QGridLayout()
37+
grid.setSpacing(6)
38+
39+
# header row: blank, X, Y, Z
40+
grid.addWidget(QLabel(""), 0, 0)
41+
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
42+
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
43+
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)
44+
45+
# Nsteps row
46+
grid.addWidget(QLabel("Nsteps:"), 1, 0)
47+
self.nsteps_x = QDoubleSpinBox()
48+
self.nsteps_y = QDoubleSpinBox()
49+
self.nsteps_z = QDoubleSpinBox()
50+
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
51+
sb.setRange(1, 1_000_000)
52+
sb.setDecimals(0)
53+
sb.setSingleStep(1)
54+
sb.setAlignment(Qt.AlignRight)
55+
grid.addWidget(self.nsteps_x, 1, 1)
56+
grid.addWidget(self.nsteps_y, 1, 2)
57+
grid.addWidget(self.nsteps_z, 1, 3)
58+
59+
# Elements row (span columns)
60+
grid.addWidget(QLabel("Elements:"), 2, 0)
61+
self.nelements = QDoubleSpinBox()
62+
self.nelements.setRange(1, 1_000_000_000)
63+
self.nelements.setDecimals(0)
64+
self.nelements.setSingleStep(100)
65+
self.nelements.setAlignment(Qt.AlignRight)
66+
grid.addWidget(self.nelements, 2, 1, 1, 3)
67+
68+
inner_layout.addLayout(grid)
69+
70+
# Place the inner layout into a QGIS collapsible group box so it matches other sections
71+
group = QgsCollapsibleGroupBox()
72+
group.setTitle("Bounding Box")
73+
group.setLayout(inner_layout)
74+
75+
# Outer layout for this widget contains the group box (so it can be treated as a single section)
76+
outer_layout = QVBoxLayout(self)
77+
outer_layout.setContentsMargins(0, 0, 0, 0)
78+
outer_layout.setSpacing(0)
79+
outer_layout.addWidget(group)
80+
81+
# initialise values from bounding box if available
82+
bb = self._get_bounding_box()
83+
if bb is not None:
84+
try:
85+
if getattr(bb, 'nsteps', None) is not None:
86+
self.nsteps_x.setValue(int(bb.nsteps[0]))
87+
self.nsteps_y.setValue(int(bb.nsteps[1]))
88+
self.nsteps_z.setValue(int(bb.nsteps[2]))
89+
except Exception:
90+
self.nsteps_x.setValue(100)
91+
self.nsteps_y.setValue(100)
92+
self.nsteps_z.setValue(1)
93+
try:
94+
if getattr(bb, 'nelements', None) is not None:
95+
self.nelements.setValue(int(getattr(bb, 'nelements')))
96+
except Exception:
97+
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
98+
else:
99+
self.nsteps_x.setValue(100)
100+
self.nsteps_y.setValue(100)
101+
self.nsteps_z.setValue(1)
102+
self.nelements.setValue(1000)
103+
104+
# connect signals
105+
self.nelements.valueChanged.connect(self._on_nelements_changed)
106+
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
107+
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
108+
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)
109+
110+
# register update callback so this widget stays in sync
111+
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
112+
try:
113+
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
114+
except Exception:
115+
pass
116+
117+
def _get_bounding_box(self):
118+
bounding_box = None
119+
if self.data_manager is not None:
120+
try:
121+
if hasattr(self.data_manager, 'get_bounding_box'):
122+
bounding_box = self.data_manager.get_bounding_box()
123+
elif hasattr(self.data_manager, 'bounding_box'):
124+
bounding_box = getattr(self.data_manager, 'bounding_box')
125+
except Exception:
126+
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
127+
bounding_box = None
128+
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
129+
try:
130+
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
131+
except Exception:
132+
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
133+
bounding_box = None
134+
return bounding_box
135+
136+
def _on_nelements_changed(self, val):
137+
bb = self._get_bounding_box()
138+
if bb is None:
139+
return
140+
try:
141+
bb.nelements = int(val)
142+
except Exception:
143+
bb.nelements = val
144+
if self.model_manager is not None:
145+
try:
146+
self.model_manager.update_bounding_box(bb)
147+
except Exception:
148+
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
149+
# refresh from authoritative source
150+
self._refresh_bb_ui()
151+
152+
def _on_nsteps_changed(self, _):
153+
bb = self._get_bounding_box()
154+
if bb is None:
155+
return
156+
try:
157+
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
158+
except Exception:
159+
try:
160+
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
161+
except Exception:
162+
pass
163+
if self.model_manager is not None:
164+
try:
165+
self.model_manager.update_bounding_box(bb)
166+
except Exception:
167+
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
168+
# refresh from authoritative source
169+
self._refresh_bb_ui()
170+
171+
def _refresh_bb_ui(self):
172+
bb = self._get_bounding_box()
173+
if bb is not None:
174+
try:
175+
self._on_bounding_box_updated(bb)
176+
except Exception:
177+
pass
178+
179+
def _on_bounding_box_updated(self, bounding_box):
180+
# collect spinboxes
181+
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
182+
for sb in spinboxes:
183+
try:
184+
sb.blockSignals(True)
185+
except Exception:
186+
pass
187+
try:
188+
if getattr(bounding_box, 'nelements', None) is not None:
189+
try:
190+
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
191+
except Exception:
192+
try:
193+
self.nelements.setValue(getattr(bounding_box, 'nelements'))
194+
except Exception:
195+
logger.debug('Could not set nelements', exc_info=True)
196+
if getattr(bounding_box, 'nsteps', None) is not None:
197+
try:
198+
nsteps = list(bounding_box.nsteps)
199+
except Exception:
200+
try:
201+
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
202+
except Exception:
203+
nsteps = None
204+
if nsteps is not None:
205+
try:
206+
self.nsteps_x.setValue(int(nsteps[0]))
207+
self.nsteps_y.setValue(int(nsteps[1]))
208+
self.nsteps_z.setValue(int(nsteps[2]))
209+
except Exception:
210+
logger.debug('Could not set nsteps', exc_info=True)
211+
finally:
212+
for sb in spinboxes:
213+
try:
214+
sb.blockSignals(False)
215+
except Exception:
216+
pass

0 commit comments

Comments
 (0)