wolfhece.wolf_sculpt
wolf_sculpt.py — Sculpting tools for Wolf digital terrain arrays.
- Inspired by Blender’s sculpt mode, adapted for DTM/DEM editing:
Smooth — weighted average of neighbours within radius
Raise — progressively raise terrain
Lower — progressively lower (dig) terrain
Flatten — project cells toward a target elevation
Noise — add random micro-relief
- Each operation is controlled by a
SculptBrushthat stores: brush shape (circle / square)
falloff curve (constant / linear / gaussian / sphere)
radius (world units, typically metres)
intensity (blend factor 0–1)
strength (height delta per stroke for raise/lower/noise, metres)
flatten_value (target elevation for flatten mode)
A SculptPanel wx.Frame lets the user tweak all parameters and
activate sculpting on the currently selected array in a WolfMapViewer.
Module Contents
- class wolfhece.wolf_sculpt.SculptMode(*args, **kwds)[source]
Bases:
enum.Enum
Create a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3
Access them by:
attribute access:
>>> Color.RED <Color.RED: 1>
value lookup:
>>> Color(1) <Color.RED: 1>
name lookup:
>>> Color['RED'] <Color.RED: 1>
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3
>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
- class wolfhece.wolf_sculpt.FalloffType(*args, **kwds)[source]
Bases:
enum.Enum
Create a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3
Access them by:
attribute access:
>>> Color.RED <Color.RED: 1>
value lookup:
>>> Color(1) <Color.RED: 1>
name lookup:
>>> Color['RED'] <Color.RED: 1>
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3
>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
- class wolfhece.wolf_sculpt.BrushShape(*args, **kwds)[source]
Bases:
enum.Enum
Create a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3
Access them by:
attribute access:
>>> Color.RED <Color.RED: 1>
value lookup:
>>> Color(1) <Color.RED: 1>
name lookup:
>>> Color['RED'] <Color.RED: 1>
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3
>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
- class wolfhece.wolf_sculpt.ProfileShape(*args, **kwds)[source]
Bases:
enum.Enum
Cross-section profile shape for the segment brush.
- wolfhece.wolf_sculpt._PROFILE_RUN_RISE: dict[ProfileShape, float][source]
- class wolfhece.wolf_sculpt.SculptBrush[source]
Applies sculpting operations to a WolfArray at a given world position.
All operations are fully vectorised using NumPy; masked (null) cells are never modified. An internal undo stack (up to MAX_UNDO strokes) lets the user step back one stroke at a time.
- mode: SculptMode[source]
- falloff: FalloffType[source]
- shape: BrushShape[source]
- _build_kernel(dx: float, dy: float)[source]
Pre-compute (di, dj, weights) in grid coordinates.
di, dj are integer offsets from the brush centre cell. weights already include intensity scaling. This is called ONCE when any brush parameter changes.
- _get_kernel(dx: float, dy: float)[source]
Return cached (di, dj, w) kernel; rebuild if params changed.
- _compute_weights(array, cx: float, cy: float)[source]
Return (ii_flat, jj_flat, w_flat, ctx) or None if brush misses.
Uses the pre-computed kernel in grid coordinates.
- apply(array, cx: float, cy: float, pressure: float = 1.0)[source]
Apply the current brush to array at world position (cx, cy).
pressure is a tablet stylus value in
[0, 1]. When1.0(default / mouse) the brush behaves normally. Lower values scale intensity and strength proportionally so that a light touch produces a weaker effect.Returns (ii, jj) — the 1-D index arrays of modified cells — or None if the brush missed all valid cells.
- _apply_smooth(data, ii, jj, w, ctx)[source]
Smooth local region with a Gaussian (or box) filter, then blend.
- _compute_auto_flatten_target(array, cx: float, cy: float) float[source]
Return the median elevation of unmasked cells in a 2×-radius zone.
Used when
flatten_autois True; falls back toself.flatten_valuewhen no valid cells are found.
- _compute_auto_flatten_plane(array, cx: float, cy: float) tuple[float, float, float] | None[source]
Fit a least-squares plane z = sx*(x-cx) + sy*(y-cy) + z_ref to the 2×-radius neighbourhood.
Returns (slope_x, slope_y, z_ref) or None if fewer than 3 valid (unmasked) cells are available.
- undo(array) tuple | None[source]
Restore the last saved snapshot.
Returns (ii, jj) of the restored cells so the caller can do a partial GL update, or None if the stack is empty.
- class wolfhece.wolf_sculpt.ProfileBrush[source]
Segment-based cross-section profile brush.
The user clicks a point on the bank (foot or head, controlled by
click_is_foot). For each click the brush:Estimates the local steepest-ascent direction from a frozen reference array (snapshot taken once when the mode is activated). The gradient is Gaussian-weighted over
gradient_search_radiusto produce a spatially smooth, stable orientation even when painting progressively along a bank from upstream to downstream.Computes the segment from the clicked point in that direction. Segment length =
bank_height × run_over_rise(deduced from the chosenprofile_shape).For every grid cell inside a perpendicular corridor of half-width
corridor_half_width, blends the current elevation toward the target elevation given by the normalised profile. A cosine falloff is applied perpendicular to the segment axis so that the modification feathers smoothly into the surrounding terrain.Optionally restricts the modification to excavation only (
allow_cut_only=True), which is the default so that a natural bank replaces a vertical wall without raising the thalweg.
All internal computations use the normalised parameter t ∈ [0, 1]. The user always works in world units (metres):
bank_height,corridor_half_width,gradient_search_radius.- profile_shape: ProfileShape[source]
- property segment_length: float[source]
Horizontal length of the profile segment, deduced from
bank_heightand the slope ratio of the selected shape (m).
- profile_z_normalized(t: numpy.ndarray) numpy.ndarray[source]
Return normalised elevation z ∈ [0, 1] at parameter t ∈ [0, 1].
t = 0 corresponds to the foot of the bank (z = z_foot); t = 1 corresponds to the head (z = z_foot + bank_height).
- freeze_reference(wa) None[source]
Freeze the current array as the gradient-estimation reference.
Must be called once when the segment mode is activated. The frozen copy is used for every subsequent stroke so that progressive bank shaping never biases its own orientation estimates.
- clear_eikonal_cache() None[source]
Discard any cached eikonal direction (reverts to Gaussian gradient).
- refine_direction_eikonal(cx: float, cy: float, wa) tuple[float, float][source]
Compute a robust slope direction at (cx, cy) using the Fast-Marching (eikonal) method and store the result in the cache.
A local patch of radius
3 × gradient_search_radiusis extracted from the frozen reference (or live array when no snapshot exists). The eikonal equation|∇T| = 1 / Fis solved isotropically withF = ‖∇z‖ + ε, so that the front advances faster where the terrain is steeper. The negative gradient of the arrival-time field−∇Tat the source cell gives the steepest-ascent direction that is globally consistent with the topography — not just a local finite-difference estimate.The result is stored in
_eikonal_dir_cache. Subsequent calls to_estimate_gradient_dir()return the cached direction directly when the query point lies withingradient_search_radiusof the cached position. Callclear_eikonal_cache()to revert to the Gaussian estimator.Returns
(ux, uy)— a unit vector — or(1.0, 0.0)on failure.
- _estimate_gradient_dir(cx: float, cy: float, wa) tuple[float, float][source]
Return the steepest-ascent unit vector at (cx, cy).
If a cached eikonal direction exists (from a prior call to
refine_direction_eikonal()) and the query point is withingradient_search_radiusof the cached position, that direction is returned directly without any new computation.Otherwise the gradient is computed from the frozen reference array (or the current array data if no snapshot exists) and spatially weighted by a Gaussian kernel of radius
gradient_search_radius.Returns
(ux, uy)— a unit vector — or(1.0, 0.0)when the local gradient magnitude is too small to be reliable.
- apply(wa, cx: float, cy: float) list[tuple[int, int, float, float]] | None[source]
Apply the profile at the clicked world point (cx, cy).
Returns a list of
(ix, iy, old_z, new_z)tuples for the cells that were modified, or None if no cells were changed.
- class wolfhece.wolf_sculpt.SculptPanel(parent, mapviewer)[source]
Bases:
wx.Frame
Floating panel for sculpting brush parameters.
Stays on top of the viewer. The Activate sculpting toggle button calls
WolfMapViewer.start_action()/end_actionwith the'sculpt'action string so that left-click / drag on the canvas applies the brush on the active array.- sync_angle_only() None[source]
Lightweight update during interactive rotation — only the angle slider.
- _update_falloff_preview() None[source]
Redraw the falloff weight curve in the embedded matplotlib figure.
For modes that use strength (Raise / Lower / Noise) the Y axis shows the effective height delta in metres (strength × weight) rather than the dimensionless weight. For all other modes the Y axis remains the dimensionless weight [0, 1].
- class wolfhece.wolf_sculpt.ProfilePanel(parent, mapviewer)[source]
Bases:
wx.Frame
Floating panel for the segment-based cross-section profile brush.
Stays on top of the viewer. The Activate toggle starts the
'profile'action so that left-clicks on the canvas apply the brush.