Files
pyGUI/src/pygui/backend/attic/marching_squares.py
T
jack 5514653b94 refactor: prune orphaned attic modules and standardize project configurations
- Move orphaned modules (data_segment.py, contour_models.py, marching_squares.py) to attic/.
- Standardize local settings logic, serial port parameters, and graph view plots.
- Update pyproject.toml pyside6-project dependencies and configure gitignore.
2026-06-18 17:09:24 -07:00

140 lines
4.7 KiB
Python

from typing import List, Optional, Tuple
import numpy as np
from pygui.backend.attic.contour_models import ContourLine, ContourSegment
# ===== Contour Generation =====
class MarchingSquares:
# ===== Public API =====
@staticmethod
def generate_contours(grid: np.ndarray, levels: List[float]) -> List[ContourLine]:
"""
Generate contour lines for a 2D grid at specified levels.
Args:
grid: 2D numpy array (shape: [width, height])
levels: List of contour levels to compute
Returns:
List of ContourLine objects
"""
if grid.size == 0:
return []
width, height = grid.shape[0], grid.shape[1]
contours = []
for level in levels:
contour = ContourLine(level=level)
# Iterate over each cell (x, y) in the grid
for y in range(height - 1):
for x in range(width - 1):
v0 = float(grid[x, y]) # top-left
v1 = float(grid[x + 1, y]) # top-right
v2 = float(grid[x + 1, y + 1]) # bottom-right
v3 = float(grid[x, y + 1]) # bottom-left
if any(np.isnan([v0, v1, v2, v3])):
continue # Skip cells with NaN values
state = (
(1 if v0 > level else 0)
| (2 if v1 > level else 0)
| (4 if v2 > level else 0)
| (8 if v3 > level else 0)
)
seg = MarchingSquares._get_segment(
x, y, v0, v1, v2, v3, level, state
)
if seg is not None:
contour.segments.append(seg)
contours.append(contour)
return contours
# ===== Geometry Helpers =====
@staticmethod
def _lerp(
x1: float,
y1: float,
x2: float,
y2: float,
val1: float,
val2: float,
level: float,
) -> Tuple[float, float]:
"""Linear interpolation between (x1,y1) and (x2,y2)."""
if val2 == val1:
return (x1 + x2) / 2, (y1 + y2) / 2
t = (level - val1) / (val2 - val1)
px = x1 + t * (x2 - x1)
py = y1 + t * (y2 - y1)
return px, py
# ===== Segment Lookup =====
@staticmethod
def _get_segment(
x: int,
y: int,
v0: float,
v1: float,
v2: float,
v3: float,
level: float,
state: int,
) -> Optional[ContourSegment]:
"""Return a ContourSegment for the given cell and state."""
if state in (10,): # Ambiguous case — skip
return None
# Map C# states to Python logic
if state == 1 or state == 14:
start = MarchingSquares._lerp(x, y, x + 1, y, v0, v1, level)
end = MarchingSquares._lerp(x, y, x, y + 1, v0, v3, level)
elif state == 2 or state == 13:
start = MarchingSquares._lerp(x + 1, y, x + 1, y + 1, v1, v2, level)
end = MarchingSquares._lerp(x, y, x + 1, y, v0, v1, level)
elif state == 3 or state == 12:
start = MarchingSquares._lerp(x, y, x, y + 1, v0, v3, level)
end = MarchingSquares._lerp(x + 1, y, x + 1, y + 1, v1, v2, level)
elif state == 4 or state == 11:
start = MarchingSquares._lerp(x + 1, y, x + 1, y + 1, v1, v2, level)
end = MarchingSquares._lerp(x + 1, y + 1, x, y + 1, v2, v3, level)
elif state == 5:
start = MarchingSquares._lerp(x, y, x, y + 1, v0, v3, level)
end = MarchingSquares._lerp(x + 1, y, x + 1, y + 1, v1, v2, level)
elif state == 6 or state == 9:
start = MarchingSquares._lerp(x, y, x + 1, y, v0, v1, level)
end = MarchingSquares._lerp(x + 1, y + 1, x, y + 1, v2, v3, level)
elif state == 7 or state == 8:
start = MarchingSquares._lerp(x, y, x, y + 1, v0, v3, level)
end = MarchingSquares._lerp(x + 1, y + 1, x, y + 1, v2, v3, level)
else:
return None
return ContourSegment(
start_x=start[0], start_y=start[1], end_x=end[0], end_y=end[1]
)
# ===== Color Mapping =====
@staticmethod
def color_from_level(
value: float, min_val: float, max_val: float
) -> Tuple[int, int, int]:
"""Return (R, G, B) tuple for a value between min and max."""
range_val = max_val - min_val
if range_val == 0:
t = 0.5
else:
t = max(0.0, min(1.0, (value - min_val) / range_val))
r = int(255 * t)
b = int(255 * (1 - t))
return (r, 0, b)