"""Path3D utilities for manufacturing post-processing.
This module provides functions for evaluating and manipulating path3d
objects, which are essential for beam segmentation operations.
"""
import math
from typing import Any, Dict, List, Optional, Tuple
def _normalize_vector(v: List[float]) -> List[float]:
"""Normalize a 3D vector to unit length."""
length = math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
if length < 1e-10:
return [0.0, 0.0, 1.0] # Default up vector
return [v[0]/length, v[1]/length, v[2]/length]
def _vector_subtract(a: List[float], b: List[float]) -> List[float]:
"""Subtract two 3D vectors."""
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def _vector_add(a: List[float], b: List[float]) -> List[float]:
"""Add two 3D vectors."""
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def _vector_scale(v: List[float], s: float) -> List[float]:
"""Scale a 3D vector."""
return [v[0] * s, v[1] * s, v[2] * s]
def _cross_product(a: List[float], b: List[float]) -> List[float]:
"""Cross product of two 3D vectors."""
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
]
def _lerp_point(a: List[float], b: List[float], t: float) -> List[float]:
"""Linear interpolation between two points."""
return [
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2])
]
def _evaluate_line_segment(
start: List[float],
end: List[float],
t: float
) -> Tuple[List[float], List[float]]:
"""Evaluate a line segment at parameter t.
Args:
start: Start point [x, y, z]
end: End point [x, y, z]
t: Parameter in [0, 1]
Returns:
Tuple of (point, tangent) where tangent is unit vector
"""
point = _lerp_point(start, end, t)
tangent = _normalize_vector(_vector_subtract(end, start))
return point, tangent
def _evaluate_arc_segment(
center: List[float],
start: List[float],
end: List[float],
normal: List[float],
t: float
) -> Tuple[List[float], List[float]]:
"""Evaluate an arc segment at parameter t.
Args:
center: Arc center point
start: Start point on arc
end: End point on arc
normal: Arc plane normal (defines rotation direction)
t: Parameter in [0, 1]
Returns:
Tuple of (point, tangent) where tangent is unit vector
"""
# Vector from center to start
r_start = _vector_subtract(start, center)
radius = math.sqrt(r_start[0]**2 + r_start[1]**2 + r_start[2]**2)
# Vector from center to end
r_end = _vector_subtract(end, center)
# Normalize radius vectors
r_start_norm = _normalize_vector(r_start)
r_end_norm = _normalize_vector(r_end)
# Calculate angle using dot product
dot = r_start_norm[0]*r_end_norm[0] + r_start_norm[1]*r_end_norm[1] + r_start_norm[2]*r_end_norm[2]
dot = max(-1.0, min(1.0, dot))
total_angle = math.acos(dot)
# Check if we need to go the "long way" around
# Use cross product to determine direction
cross = _cross_product(r_start_norm, r_end_norm)
normal_norm = _normalize_vector(normal)
sign = cross[0]*normal_norm[0] + cross[1]*normal_norm[1] + cross[2]*normal_norm[2]
if sign < 0:
total_angle = 2 * math.pi - total_angle
# Interpolate angle
angle = t * total_angle
# Rodrigues' rotation formula to rotate r_start around normal
cos_a = math.cos(angle)
sin_a = math.sin(angle)
# k x r (cross product of normal and radius vector)
k_cross_r = _cross_product(normal_norm, r_start_norm)
# k . r (dot product)
k_dot_r = normal_norm[0]*r_start_norm[0] + normal_norm[1]*r_start_norm[1] + normal_norm[2]*r_start_norm[2]
# Rotated radius direction: r*cos(a) + (k x r)*sin(a) + k*(k.r)*(1-cos(a))
rotated = [
r_start_norm[i] * cos_a + k_cross_r[i] * sin_a + normal_norm[i] * k_dot_r * (1 - cos_a)
for i in range(3)
]
# Point on arc
point = _vector_add(center, _vector_scale(rotated, radius))
# Tangent is perpendicular to radius, in direction of rotation
# tangent = normal x rotated_radius_direction
tangent = _normalize_vector(_cross_product(normal_norm, rotated))
return point, tangent
[docs]
def evaluate_path3d_at_t(
path3d: Dict[str, Any],
t: float
) -> Tuple[List[float], List[float]]:
"""Evaluate a path3d at a global parameter value.
Args:
path3d: Dict with 'segments' list of line/arc segments
t: Global parameter in [0, 1] across entire path
Returns:
Tuple of (point, tangent) at parameter t
- point: [x, y, z] position on path
- tangent: [tx, ty, tz] unit tangent vector
Raises:
ValueError: If path has no segments or t is out of range
"""
segments = path3d.get('segments', [])
if not segments:
raise ValueError("Path has no segments")
# Clamp t to valid range
t = max(0.0, min(1.0, t))
# Convert global t to segment index and local t
n_segments = len(segments)
if t >= 1.0:
# At the end, evaluate last segment at t=1
seg_idx = n_segments - 1
local_t = 1.0
else:
# Find which segment and local parameter
scaled = t * n_segments
seg_idx = int(scaled)
local_t = scaled - seg_idx
seg = segments[seg_idx]
seg_type = seg.get('type', 'line')
if seg_type == 'line':
return _evaluate_line_segment(seg['start'], seg['end'], local_t)
elif seg_type == 'arc':
normal = seg.get('normal', [0, 0, 1])
return _evaluate_arc_segment(
seg['center'], seg['start'], seg['end'], normal, local_t
)
else:
raise ValueError(f"Unknown segment type: {seg_type}")
[docs]
def compute_cut_plane(
path3d: Dict[str, Any],
t: float
) -> Tuple[List[float], List[float]]:
"""Compute the cut plane at a parameter along the path.
The cut plane is perpendicular to the path tangent at the given parameter.
Args:
path3d: Dict with 'segments' list
t: Global parameter in [0, 1]
Returns:
Tuple of (point, normal) defining the plane
- point: A point on the plane (the path point at t)
- normal: Unit normal to the plane (the path tangent at t)
"""
point, tangent = evaluate_path3d_at_t(path3d, t)
return point, tangent
[docs]
def path_length(path3d: Dict[str, Any]) -> float:
"""Compute the total arc length of a path3d.
Args:
path3d: Dict with 'segments' list
Returns:
Total length in same units as path coordinates
"""
segments = path3d.get('segments', [])
total = 0.0
for seg in segments:
seg_type = seg.get('type', 'line')
if seg_type == 'line':
diff = _vector_subtract(seg['end'], seg['start'])
total += math.sqrt(diff[0]**2 + diff[1]**2 + diff[2]**2)
elif seg_type == 'arc':
# Arc length = radius * angle
center = seg['center']
start = seg['start']
end = seg['end']
normal = seg.get('normal', [0, 0, 1])
r_start = _vector_subtract(start, center)
radius = math.sqrt(r_start[0]**2 + r_start[1]**2 + r_start[2]**2)
r_end = _vector_subtract(end, center)
r_start_norm = _normalize_vector(r_start)
r_end_norm = _normalize_vector(r_end)
dot = r_start_norm[0]*r_end_norm[0] + r_start_norm[1]*r_end_norm[1] + r_start_norm[2]*r_end_norm[2]
dot = max(-1.0, min(1.0, dot))
angle = math.acos(dot)
# Check direction
cross = _cross_product(r_start_norm, r_end_norm)
normal_norm = _normalize_vector(normal)
sign = cross[0]*normal_norm[0] + cross[1]*normal_norm[1] + cross[2]*normal_norm[2]
if sign < 0:
angle = 2 * math.pi - angle
total += radius * angle
return total
[docs]
def length_to_parameter(path3d: Dict[str, Any], arc_length: float) -> float:
"""Convert an arc length to a global parameter.
Args:
path3d: Dict with 'segments' list
arc_length: Distance along the path from start
Returns:
Global parameter t in [0, 1]
"""
total_length = path_length(path3d)
if total_length < 1e-10:
return 0.0
return min(1.0, max(0.0, arc_length / total_length))
[docs]
def parameter_to_length(path3d: Dict[str, Any], t: float) -> float:
"""Convert a global parameter to arc length.
Args:
path3d: Dict with 'segments' list
t: Global parameter in [0, 1]
Returns:
Arc length from path start to parameter t
"""
# Extract sub-path and compute its length
sub_path = extract_sub_path(path3d, 0.0, t)
return path_length(sub_path)