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 SculptBrush that 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

wolfhece.wolf_sculpt._SCIPY_AVAILABLE = True[source]
class wolfhece.wolf_sculpt.SculptMode(*args, **kwds)[source]

Bases: enum.Enum

Inheritance diagram of wolfhece.wolf_sculpt.SculptMode

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.

SMOOTH = 'smooth'[source]
RAISE = 'raise'[source]
LOWER = 'lower'[source]
FLATTEN = 'flatten'[source]
FLATTEN_PLANE = 'flatten_plane'[source]
NOISE = 'noise'[source]
class wolfhece.wolf_sculpt.FalloffType(*args, **kwds)[source]

Bases: enum.Enum

Inheritance diagram of wolfhece.wolf_sculpt.FalloffType

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.

CONSTANT = 'constant'[source]
LINEAR = 'linear'[source]
GAUSSIAN = 'gaussian'[source]
SIGMOID = 'sigmoid'[source]
SPHERE = 'sphere'[source]
class wolfhece.wolf_sculpt.BrushShape(*args, **kwds)[source]

Bases: enum.Enum

Inheritance diagram of wolfhece.wolf_sculpt.BrushShape

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.

CIRCLE = 'circle'[source]
SQUARE = 'square'[source]
RECTANGLE = 'rectangle'[source]
class wolfhece.wolf_sculpt.ProfileShape(*args, **kwds)[source]

Bases: enum.Enum

Inheritance diagram of wolfhece.wolf_sculpt.ProfileShape

Cross-section profile shape for the segment brush.

SLOPE_1_1 = '1_1'[source]
SLOPE_3_2 = '3_2'[source]
SLOPE_2_1 = '2_1'[source]
SLOPE_3_1 = '3_1'[source]
CIRCULAR = 'circular'[source]
SEMI_CIRCULAR = 'semi_circular'[source]
GABION = 'gabion'[source]
CUSTOM = 'custom'[source]
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.

MAX_UNDO: int = 20[source]
mode: SculptMode[source]
falloff: FalloffType[source]
shape: BrushShape[source]
radius: float = 20.0[source]
rect_width: float = 20.0[source]
rect_height: float = 40.0[source]
rect_angle: float = 0.0[source]
intensity: float = 0.5[source]
strength: float = 0.5[source]
gaussian_sigma: float = 0.45[source]
gaussian_order: int = 1[source]
sigmoid_center: float = 0.6[source]
sigmoid_steepness: float = 12.0[source]
flatten_value: float = 0.0[source]
flatten_auto: bool = True[source]
search_zone_factor: float = 2.0[source]
plane_slope_x: float = 0.0[source]
plane_slope_y: float = 0.0[source]
plane_z_ref: float = 0.0[source]
plane_ref_x: float = 0.0[source]
plane_ref_y: float = 0.0[source]
plane_auto: bool = True[source]
_on_plane_fitted: object = None[source]
_undo: list[tuple] = [][source]
_kernel_cache: tuple | None = None[source]
_ref_array: numpy.ndarray | None = None[source]
_ref_null: float | None = None[source]
_kernel_key(dx: float, dy: float) tuple[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]. When 1.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_auto is True; falls back to self.flatten_value when 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.

_push_undo(array, ii, jj) None[source]
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.

clear_undo() None[source]
freeze_reference(wa) None[source]

Snapshot the current array state as the cut/fill baseline.

Call once when sculpting is activated so that CutFillOverlay can compute cumulative cut/fill volumes relative to the original terrain. Has no effect on sculpt operations themselves.

clear_reference() None[source]

Release the frozen reference (call when deactivating).

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:

  1. 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_radius to produce a spatially smooth, stable orientation even when painting progressively along a bank from upstream to downstream.

  2. Computes the segment from the clicked point in that direction. Segment length = bank_height × run_over_rise (deduced from the chosen profile_shape).

  3. 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.

  4. 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.

MAX_UNDO: int = 20[source]
profile_shape: ProfileShape[source]
bank_height: float = 2.0[source]
corridor_half_width: float = 1.0[source]
gradient_search_radius: float = 20.0[source]
allow_cut_only: bool = True[source]
click_is_foot: bool = True[source]
gabion_step: float = 0.5[source]
custom_profile: list[tuple[float, float]] = [][source]
_ref_array: numpy.ndarray | None = None[source]
_ref_null: float | None = None[source]
_eikonal_dir_cache: tuple[float, float, float, float] | None = None[source]
_undo: list[list[tuple[int, int, float, float]]] = [][source]
property run_over_rise: float[source]

Horizontal run per unit of vertical rise (dimensionless).

property segment_length: float[source]

Horizontal length of the profile segment, deduced from bank_height and 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_reference() None[source]

Release the frozen reference (call when deactivating the mode).

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_radius is extracted from the frozen reference (or live array when no snapshot exists). The eikonal equation |∇T| = 1 / F is solved isotropically with F = ‖∇z‖ + ε, so that the front advances faster where the terrain is steeper. The negative gradient of the arrival-time field −∇T at 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 within gradient_search_radius of the cached position. Call clear_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 within gradient_search_radius of 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.

undo(wa) list[tuple[int, int]] | None[source]

Restore the last applied stroke.

Returns a list of (ix, iy) for the restored cells, or None if the undo stack is empty.

clear_undo() None[source]
wolfhece.wolf_sculpt._MODE_CHOICES[source]
wolfhece.wolf_sculpt._FALLOFF_CHOICES[source]
wolfhece.wolf_sculpt._SHAPE_CHOICES[source]
class wolfhece.wolf_sculpt.SculptPanel(parent, mapviewer)[source]

Bases: wx.Frame

Inheritance diagram of wolfhece.wolf_sculpt.SculptPanel

Floating panel for sculpting brush parameters.

Stays on top of the viewer. The Activate sculpting toggle button calls WolfMapViewer.start_action() / end_action with the 'sculpt' action string so that left-click / drag on the canvas applies the brush on the active array.

mapviewer[source]
brush[source]
_build_ui() None[source]
_update_visibility() None[source]

Show/hide contextual sections based on current brush settings.

_on_mode(e: wx.CommandEvent) None[source]
_on_shape(e: wx.CommandEvent) None[source]
_on_rect_slider(e: wx.CommandEvent) None[source]
_on_gauss_order(e: wx.SpinEvent) None[source]
_on_sigmoid_params(e: wx.SpinDoubleEvent) None[source]
_on_sigmoid_preset(e: wx.CommandEvent) None[source]
_on_falloff(e: wx.CommandEvent) None[source]
_on_slider(e: wx.CommandEvent) None[source]
_on_flatten(e) None[source]
_on_freeze_ref(e: wx.CommandEvent) None[source]
_on_clear_ref(e: wx.CommandEvent) None[source]
_on_cutfill_toggle(e: wx.CommandEvent) None[source]
_on_activate(e: wx.CommandEvent) None[source]
_on_undo(e: wx.CommandEvent) None[source]
_on_close(e) None[source]
notify_action_ended() None[source]
sync_angle_only() None[source]

Lightweight update during interactive rotation — only the angle slider.

sync_sliders() None[source]

Push current brush values back into all UI sliders.

_on_radius_exact(e) None[source]
_on_flatten_auto(e: wx.CommandEvent) None[source]
_sync_plane_ui_state() None[source]

Enable/disable plane param fields based on auto-fit state.

_on_plane_auto(e: wx.CommandEvent) None[source]
_on_plane_param(e) None[source]
_on_plane_fit(e: wx.CommandEvent) None[source]
sync_plane_ui() None[source]

Refresh plane param UI fields from brush attributes.

_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].

update_grid_hint() None[source]

Update the cell-size hint label next to the exact-radius field.

wolfhece.wolf_sculpt._PROFILE_CHOICES[source]
class wolfhece.wolf_sculpt.ProfilePanel(parent, mapviewer)[source]

Bases: wx.Frame

Inheritance diagram of wolfhece.wolf_sculpt.ProfilePanel

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.

mapviewer[source]
brush[source]
_build_ui() None[source]
_update_visibility() None[source]
_update_info() None[source]

Refresh the deduced segment-length label.

_on_shape(e: wx.CommandEvent) None[source]
_on_param_spin(e: wx.SpinDoubleEvent) None[source]
_on_gabion_spin(e: wx.SpinDoubleEvent) None[source]
_on_cut_only(e: wx.CommandEvent) None[source]
_on_foot_head(e: wx.CommandEvent) None[source]
_on_activate(e: wx.CommandEvent) None[source]
_on_undo(e: wx.CommandEvent) None[source]
_on_close(e) None[source]
_on_cutfill_toggle(e: wx.CommandEvent) None[source]
_on_refine_eikonal(e: wx.CommandEvent) None[source]

Run the eikonal direction solver at the current cursor position.

_on_clear_eikonal(e: wx.CommandEvent) None[source]

Discard the cached eikonal direction.

notify_action_ended() None[source]
sync_from_brush() None[source]

Push current brush values back into all UI controls.

_update_preview() None[source]

Redraw the profile shape z(t) in the embedded matplotlib figure.