Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Export and visualisation improvements by lachlangrose · Pull Request #43 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
037d50f
fix: adding export of scalar field value to qgis vector pointset
lachlangrose Sep 6, 2025
3ce70a1
fix: remove feature data from data manager when its deleted
lachlangrose Sep 6, 2025
ccc03b9
fix: make fold plunge/azi attributes of panel for easy access
lachlangrose Sep 6, 2025
1dc4b2f
fix: update viewer to store meshes and a reference to actors.
lachlangrose Sep 7, 2025
437817c
fix: uses the meshes dictionary for export
lachlangrose Sep 7, 2025
6073e7f
remove typo
lachlangrose Sep 7, 2025
ea0aefc
fix: add histogram for scalar value, allow selecting scalar, choose t…
lachlangrose Sep 7, 2025
165f25f
fix: streamline color handling and scalar updates in ObjectProperties…
lachlangrose Sep 7, 2025
8bf43e1
fix: add edge visibility toggle and line width control in ObjectPrope…
lachlangrose Sep 7, 2025
742508e
fix: allow all pv objects to be loaded
lachlangrose Sep 7, 2025
8124f36
fix: use active_scalars_name to prefile scalars kwarg
lachlangrose Sep 7, 2025
352f5b1
fix: apply colourmap change and refactored actor/mapper update into s…
lachlangrose Sep 7, 2025
33b2967
fix: evalute feature onto vtk grid
lachlangrose Sep 7, 2025
a26fc49
add docs
lachlangrose Sep 7, 2025
dc7d8f2
fix: linting
lachlangrose Sep 8, 2025
5561ded
fix: use z coordinate if it exists for all manually added features
lachlangrose Sep 8, 2025
29abb3e
fix: remove print statements, rescale z for data to be in real coordi…
lachlangrose Sep 8, 2025
08a50db
fix: put all feature options in QgsCollapsibleGroupBox
lachlangrose Sep 8, 2025
50cec1b
fix: add layer from qgis to visualisation
lachlangrose Sep 8, 2025
2572749
fix: updating stratigraphic column, need update loopstructural.
lachlangrose Sep 8, 2025
efec3ab
fix: lint
lachlangrose Sep 8, 2025
c905dda
fix: upgrade loopstructural requirement
lachlangrose Sep 8, 2025
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QGridLayout,
QLabel,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
)
from qgis.gui import QgsCollapsibleGroupBox

from LoopStructural import getLogger
logger = getLogger(__name__)


class BoundingBoxWidget(QWidget):
"""Standalone bounding-box widget used in the export/evaluation panel.

Shows a compact 3-column layout for X/Y/Z nsteps and a single-row
control for the overall element count (nelements). The widget keeps
itself in sync with the authoritative bounding_box available from a
provided data_manager or model_manager and will call back to update
the model when the user changes values.
"""

def __init__(self, parent=None, *, model_manager=None, data_manager=None):
super().__init__(parent)
self.model_manager = model_manager
self.data_manager = data_manager

# Create the inner layout that will be placed inside a collapsible group widget
inner_layout = QVBoxLayout()
inner_layout.setContentsMargins(0, 0, 0, 0)
inner_layout.setSpacing(6)

grid = QGridLayout()
grid.setSpacing(6)

# header row: blank, X, Y, Z
grid.addWidget(QLabel(""), 0, 0)
grid.addWidget(QLabel("X"), 0, 1, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Y"), 0, 2, alignment=Qt.AlignCenter)
grid.addWidget(QLabel("Z"), 0, 3, alignment=Qt.AlignCenter)

# Nsteps row
grid.addWidget(QLabel("Nsteps:"), 1, 0)
self.nsteps_x = QDoubleSpinBox()
self.nsteps_y = QDoubleSpinBox()
self.nsteps_z = QDoubleSpinBox()
for sb in (self.nsteps_x, self.nsteps_y, self.nsteps_z):
sb.setRange(1, 1_000_000)
sb.setDecimals(0)
sb.setSingleStep(1)
sb.setAlignment(Qt.AlignRight)
grid.addWidget(self.nsteps_x, 1, 1)
grid.addWidget(self.nsteps_y, 1, 2)
grid.addWidget(self.nsteps_z, 1, 3)

# Elements row (span columns)
grid.addWidget(QLabel("Elements:"), 2, 0)
self.nelements = QDoubleSpinBox()
self.nelements.setRange(1, 1_000_000_000)
self.nelements.setDecimals(0)
self.nelements.setSingleStep(100)
self.nelements.setAlignment(Qt.AlignRight)
grid.addWidget(self.nelements, 2, 1, 1, 3)

inner_layout.addLayout(grid)

# Place the inner layout into a QGIS collapsible group box so it matches other sections
group = QgsCollapsibleGroupBox()
group.setTitle("Bounding Box")
group.setLayout(inner_layout)

# Outer layout for this widget contains the group box (so it can be treated as a single section)
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.setSpacing(0)
outer_layout.addWidget(group)

# initialise values from bounding box if available
bb = self._get_bounding_box()
if bb is not None:
try:
if getattr(bb, 'nsteps', None) is not None:
self.nsteps_x.setValue(int(bb.nsteps[0]))
self.nsteps_y.setValue(int(bb.nsteps[1]))
self.nsteps_z.setValue(int(bb.nsteps[2]))
except Exception:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
try:
if getattr(bb, 'nelements', None) is not None:
self.nelements.setValue(int(getattr(bb, 'nelements')))
except Exception:
self.nelements.setValue(getattr(bb, 'nelements', 1000) if bb is not None else 1000)
else:
self.nsteps_x.setValue(100)
self.nsteps_y.setValue(100)
self.nsteps_z.setValue(1)
self.nelements.setValue(1000)

# connect signals
self.nelements.valueChanged.connect(self._on_nelements_changed)
self.nsteps_x.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_y.valueChanged.connect(self._on_nsteps_changed)
self.nsteps_z.valueChanged.connect(self._on_nsteps_changed)

# register update callback so this widget stays in sync
if self.data_manager is not None and hasattr(self.data_manager, 'set_bounding_box_update_callback'):
try:
self.data_manager.set_bounding_box_update_callback(self._on_bounding_box_updated)
except Exception:
pass

def _get_bounding_box(self):
bounding_box = None
if self.data_manager is not None:
try:
if hasattr(self.data_manager, 'get_bounding_box'):
bounding_box = self.data_manager.get_bounding_box()
elif hasattr(self.data_manager, 'bounding_box'):
bounding_box = getattr(self.data_manager, 'bounding_box')
except Exception:
logger.debug('Failed to get bounding box from data_manager', exc_info=True)
bounding_box = None
if bounding_box is None and self.model_manager is not None and getattr(self.model_manager, 'model', None) is not None:
try:
bounding_box = getattr(self.model_manager.model, 'bounding_box', None)
except Exception:
logger.debug('Failed to get bounding box from model_manager', exc_info=True)
bounding_box = None
return bounding_box

def _on_nelements_changed(self, val):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nelements = int(val)
except Exception:
bb.nelements = val
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _on_nsteps_changed(self, _):
bb = self._get_bounding_box()
if bb is None:
return
try:
bb.nsteps = np.array([int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())])
except Exception:
try:
bb.nsteps = [int(self.nsteps_x.value()), int(self.nsteps_y.value()), int(self.nsteps_z.value())]
except Exception:
pass
if self.model_manager is not None:
try:
self.model_manager.update_bounding_box(bb)
except Exception:
logger.debug('Failed to update bounding_box on model_manager', exc_info=True)
# refresh from authoritative source
self._refresh_bb_ui()

def _refresh_bb_ui(self):
bb = self._get_bounding_box()
if bb is not None:
try:
self._on_bounding_box_updated(bb)
except Exception:
pass

def _on_bounding_box_updated(self, bounding_box):
# collect spinboxes
spinboxes = [self.nelements, self.nsteps_x, self.nsteps_y, self.nsteps_z]
for sb in spinboxes:
try:
sb.blockSignals(True)
except Exception:
pass
try:
if getattr(bounding_box, 'nelements', None) is not None:
try:
self.nelements.setValue(int(getattr(bounding_box, 'nelements')))
except Exception:
try:
self.nelements.setValue(getattr(bounding_box, 'nelements'))
except Exception:
logger.debug('Could not set nelements', exc_info=True)
if getattr(bounding_box, 'nsteps', None) is not None:
try:
nsteps = list(bounding_box.nsteps)
except Exception:
try:
nsteps = [int(bounding_box.nsteps[0]), int(bounding_box.nsteps[1]), int(bounding_box.nsteps[2])]
except Exception:
nsteps = None
if nsteps is not None:
try:
self.nsteps_x.setValue(int(nsteps[0]))
self.nsteps_y.setValue(int(nsteps[1]))
self.nsteps_z.setValue(int(nsteps[2]))
except Exception:
logger.debug('Could not set nsteps', exc_info=True)
finally:
for sb in spinboxes:
try:
sb.blockSignals(False)
except Exception:
pass
Loading