Skip to content

Commit 94669f8

Browse files
lachlangroseclaude
andcommitted
feat: add topography surface and cross-section tools to the 3D viewer
Add "Add Topography Surface" and "Colour Topography by Stratigraphic Column" to the visualisation panel: samples the DEM across the model extent, shades it by elevation by default, and can instead colour it by evaluating the stratigraphic column at each point (via model_manager.evaluate_stratigraphy_on_points + stratigraphic_ids_to_rgb), with lighting disabled on that mode so colours read as true/flat rather than shaded by DEM slope. Both the DEM sampling and the stratigraphy evaluation run on a background thread via background_task.py, matching the pattern used by the other map2loop-backed widgets. Add plane and line-extrusion cross-section tools (cross_section_utils build_plane_mesh/build_line_extrusion_mesh), coloured by the same stratigraphic-column pipeline. Thread data_manager through LoopWidget -> VisualisationWidget -> FeatureListWidget so the cross-section tools have CRS-aware access to project layers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9ee4853 commit 94669f8

5 files changed

Lines changed: 636 additions & 4 deletions

File tree

loopstructural/gui/loop_widget.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ def __init__(
4848
)
4949

5050
self.visualisation_widget = VisualisationWidget(
51-
self, mapCanvas=self.mapCanvas, logger=self.logger, model_manager=self.model_manager
51+
self,
52+
mapCanvas=self.mapCanvas,
53+
logger=self.logger,
54+
data_manager=self.data_manager,
55+
model_manager=self.model_manager,
5256
)
5357
tabWidget.addTab(self.modelling_widget, "Modelling")
5458
tabWidget.addTab(self.visualisation_widget, "Visualisation")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Pure-geometry helpers for building cross-section meshes.
2+
3+
No Qt/pyvista-viewer or QGIS dependency -- callers (e.g. FeatureListWidget)
4+
sample points from the returned meshes and colour them via
5+
`mesh_scalar_utils.stratigraphic_ids_to_rgb`.
6+
"""
7+
8+
import numpy as np
9+
import pyvista as pv
10+
11+
12+
def build_plane_mesh(origin, normal, size: float, resolution: int = 50) -> pv.PolyData:
13+
"""Build a square plane mesh centred at `origin`, oriented by `normal`.
14+
15+
Parameters
16+
----------
17+
origin : array_like
18+
(3,) plane centre.
19+
normal : array_like
20+
(3,) plane normal; does not need to be unit length.
21+
size : float
22+
Edge length of the (square) plane.
23+
resolution : int, optional
24+
Number of sample points along each edge, by default 50.
25+
"""
26+
return pv.Plane(
27+
center=origin,
28+
direction=normal,
29+
i_size=size,
30+
j_size=size,
31+
i_resolution=resolution,
32+
j_resolution=resolution,
33+
)
34+
35+
36+
def resample_polyline(coords_xy, n: int) -> np.ndarray:
37+
"""Resample a 2D polyline to `n` points, evenly spaced by arc length."""
38+
coords = np.asarray(coords_xy, dtype=float)
39+
seg_lengths = np.hypot(*np.diff(coords, axis=0).T)
40+
cumulative = np.concatenate([[0.0], np.cumsum(seg_lengths)])
41+
total_length = cumulative[-1]
42+
if total_length == 0:
43+
return np.repeat(coords[:1], n, axis=0)
44+
targets = np.linspace(0.0, total_length, n)
45+
x = np.interp(targets, cumulative, coords[:, 0])
46+
y = np.interp(targets, cumulative, coords[:, 1])
47+
return np.column_stack([x, y])
48+
49+
50+
def build_line_extrusion_mesh(
51+
coords_xy, z_min: float, z_max: float, resolution: int = 50, z_resolution: int = 50
52+
) -> pv.StructuredGrid:
53+
"""Build a vertical cross-section by extruding a 2D line through a Z range.
54+
55+
Parameters
56+
----------
57+
coords_xy : array_like
58+
(M, 2) ordered line vertices, in the model's coordinate system.
59+
z_min, z_max : float
60+
Vertical extent of the extrusion.
61+
resolution : int, optional
62+
Number of points sampled along the line (by arc length), by default 50.
63+
z_resolution : int, optional
64+
Number of points sampled vertically, by default 50.
65+
"""
66+
xy = resample_polyline(coords_xy, resolution)
67+
z = np.linspace(z_min, z_max, z_resolution)
68+
xx = np.tile(xy[:, 0][:, None], (1, z_resolution))
69+
yy = np.tile(xy[:, 1][:, None], (1, z_resolution))
70+
zz = np.tile(z[None, :], (resolution, 1))
71+
return pv.StructuredGrid(xx, yy, zz)

0 commit comments

Comments
 (0)