98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
"""Straighten path — extract cross-section along a curved spline path."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
from scipy.ndimage import map_coordinates
|
|
|
|
from backend.node_registry import register_node
|
|
from backend.data_types import DataField
|
|
|
|
|
|
@register_node(display_name="Straighten Path")
|
|
class StraightenPath:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"field": ("DATA_FIELD",),
|
|
"points_x": ("STRING", {"default": "0.25, 0.5, 0.75"}),
|
|
"points_y": ("STRING", {"default": "0.5, 0.3, 0.5"}),
|
|
"thickness": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
|
|
"n_samples": ("INT", {"default": 256, "min": 10, "max": 2048, "step": 1}),
|
|
}
|
|
}
|
|
|
|
OUTPUTS = (
|
|
('DATA_FIELD', 'straightened'),
|
|
)
|
|
FUNCTION = "process"
|
|
|
|
DESCRIPTION = (
|
|
"Extract a cross-section along an arbitrary curved path defined by "
|
|
"control points. Points are given as fractional coordinates (0-1). "
|
|
"The path is interpolated with cubic splines, and data is sampled "
|
|
"along it with configurable thickness. "
|
|
)
|
|
|
|
KEYWORDS = ("unbend", "unroll", "spline", "curved profile", "extract path")
|
|
|
|
def process(self, field: DataField, points_x: str, points_y: str,
|
|
thickness: int, n_samples: int) -> tuple:
|
|
data = np.asarray(field.data, dtype=np.float64)
|
|
yres, xres = data.shape
|
|
|
|
# Parse control points
|
|
px = [float(v.strip()) * (xres - 1) for v in points_x.split(",") if v.strip()]
|
|
py = [float(v.strip()) * (yres - 1) for v in points_y.split(",") if v.strip()]
|
|
|
|
if len(px) < 2 or len(py) < 2:
|
|
# Need at least 2 points
|
|
return (field,)
|
|
|
|
n_pts = min(len(px), len(py))
|
|
px, py = px[:n_pts], py[:n_pts]
|
|
|
|
# Parameterize path and interpolate
|
|
t_ctrl = np.linspace(0, 1, n_pts)
|
|
t_sample = np.linspace(0, 1, n_samples)
|
|
|
|
# Simple cubic interpolation via numpy
|
|
if n_pts >= 4:
|
|
from numpy.polynomial.polynomial import Polynomial
|
|
cx = np.interp(t_sample, t_ctrl, px)
|
|
cy = np.interp(t_sample, t_ctrl, py)
|
|
else:
|
|
cx = np.interp(t_sample, t_ctrl, px)
|
|
cy = np.interp(t_sample, t_ctrl, py)
|
|
|
|
# Sample along path with thickness
|
|
if thickness <= 1:
|
|
values = map_coordinates(data, [cy, cx], order=1, mode='nearest')
|
|
result = values.reshape(1, -1)
|
|
else:
|
|
# Compute normals
|
|
dcx = np.gradient(cx)
|
|
dcy = np.gradient(cy)
|
|
length = np.sqrt(dcx**2 + dcy**2)
|
|
length = np.maximum(length, 1e-10)
|
|
nx = -dcy / length
|
|
ny = dcx / length
|
|
|
|
offsets = np.linspace(-(thickness - 1) / 2, (thickness - 1) / 2, thickness)
|
|
result = np.zeros((thickness, n_samples))
|
|
for i, off in enumerate(offsets):
|
|
sx = cx + off * nx
|
|
sy = cy + off * ny
|
|
result[i] = map_coordinates(data, [sy, sx], order=1, mode='nearest')
|
|
|
|
# Physical dimensions
|
|
total_length = 0.0
|
|
for i in range(1, len(cx)):
|
|
dx_phys = (cx[i] - cx[i - 1]) * field.dx
|
|
dy_phys = (cy[i] - cy[i - 1]) * field.dy
|
|
total_length += np.sqrt(dx_phys**2 + dy_phys**2)
|
|
|
|
return (field.replace(data=result, xreal=total_length,
|
|
yreal=thickness * max(field.dx, field.dy)),)
|