Source code for wolfhece.wolf_array._hillshade_params

"""
Per-array hillshade material parameters.

Each :class:`WolfArray` carries a :class:`HillshadeRenderParams` instance
that stores the material properties used by the per-pixel Blinn-Phong
hillshade shader.  Sun position and global enable flags remain on the
viewer (:class:`WolfMapViewer`).

The viewer can operate in **synchronised** mode (all arrays share one
set of material parameters) or **per-array** mode (each array has its
own parameters, and the panel reflects the active array).

Author: HECE - University of Liege
Date: 2026

Copyright (c) 2026 University of Liege. All rights reserved.
"""

from __future__ import annotations

import json
import logging
from pathlib import Path

[docs] logger = logging.getLogger(__name__)
# Sidecar file suffix appended to WolfArray.filename
[docs] SIDECAR_SUFFIX = '.hillshade.json'
[docs] class HillshadeRenderParams: """Material parameters for per-pixel Blinn-Phong hillshade. These are the parameters that **differ per WolfArray** when running in per-array mode. Sun position, enable flag and multi-directional flag remain on the viewer. All values are stored in *user-facing* convention: * **glossiness** ∈ [0, 1]: 0 = matte (broad highlight), 1 = mirror (sharp highlight). The shader receives ``roughness = 1 − glossiness``. * **highlight** ∈ [0, 1]: 0 = palette-tinted specular (subtle), 1 = pure white specular (prominent). The shader receives ``metallic = 1 − highlight``. """ __slots__ = ('z_exaggeration', 'specular', 'glossiness', 'highlight') def __init__(self, *, z_exaggeration: float = 1.0, specular: float = 0.0, glossiness: float = 0.5, highlight: float = 1.0):
[docs] self.z_exaggeration = z_exaggeration
[docs] self.specular = specular
[docs] self.glossiness = glossiness
[docs] self.highlight = highlight
# ---- Cloning ----
[docs] def copy(self) -> "HillshadeRenderParams": return HillshadeRenderParams( z_exaggeration=self.z_exaggeration, specular=self.specular, glossiness=self.glossiness, highlight=self.highlight, )
[docs] def copy_from(self, other: "HillshadeRenderParams") -> None: """Copy all values from *other* into *self*.""" self.z_exaggeration = other.z_exaggeration self.specular = other.specular self.glossiness = other.glossiness self.highlight = other.highlight
# ---- Serialisation ----
[docs] def to_dict(self) -> dict: return { 'z_exaggeration': self.z_exaggeration, 'specular': self.specular, 'glossiness': self.glossiness, 'highlight': self.highlight, }
@classmethod
[docs] def from_dict(cls, d: dict) -> "HillshadeRenderParams": return cls( z_exaggeration=float(d.get('z_exaggeration', 1.0)), specular=float(d.get('specular', 0.0)), glossiness=float(d.get('glossiness', 0.5)), highlight=float(d.get('highlight', 1.0)), )
# ---- JSON sidecar persistence ----
[docs] def save(self, array_filename: str | Path) -> None: """Write a ``*.hillshade.json`` sidecar next to *array_filename*.""" path = Path(str(array_filename) + SIDECAR_SUFFIX) try: with open(path, 'w', encoding='utf-8') as f: json.dump(self.to_dict(), f, indent=2) except OSError: logger.warning('Could not write hillshade params to %s', path)
@classmethod
[docs] def load(cls, array_filename: str | Path) -> "HillshadeRenderParams | None": """Load from sidecar if it exists, else return *None*.""" path = Path(str(array_filename) + SIDECAR_SUFFIX) if not path.is_file(): return None try: with open(path, 'r', encoding='utf-8') as f: return cls.from_dict(json.load(f)) except (OSError, json.JSONDecodeError, KeyError): logger.warning('Could not read hillshade params from %s', path) return None
# ---- Equality (for tests & sync logic) ---- def __eq__(self, other): if not isinstance(other, HillshadeRenderParams): return NotImplemented return (self.z_exaggeration == other.z_exaggeration and self.specular == other.specular and self.glossiness == other.glossiness and self.highlight == other.highlight) def __repr__(self): return (f'HillshadeRenderParams(z_exaggeration={self.z_exaggeration}, ' f'specular={self.specular}, glossiness={self.glossiness}, ' f'highlight={self.highlight})')