Expand to view the full python code, Section 1 - 11
import Rhino.Geometry as rg
from System.Drawing import Color
import math
import sys
import time
### SECTION 2: EASING AND BLENDING FUNCTIONS
def apply_easing(value, exaggeration, easing_type="cosine", strength=1.0):
"""
Apply different easing functions to a normalized value (0-1)
Parameters:
value: Normalized value (0-1)
exaggeration: Controls the "contrast" (0-1)
0 = linear blend, 1 = full easing
easing_type: Type of easing to apply ("cosine" or "hermite")
strength: For hermite, controls the curve shape (0.5-2.0 typical)
Returns:
Blended value (0-1)
"""
# Ensure inputs are in valid range
value = max(0.0, min(1.0, value))
exaggeration = max(0.0, min(1.0, exaggeration))
# Calculate appropriate blend based on easing type
if easing_type == "cosine":
# Cosine easing
eased_component = (1.0 - math.cos(math.pi * value)) * 0.5
elif easing_type == "hermite":
# Hermite ease-out with adjustable strength
eased_component = hermite_ease_out(value, strength)
else:
# Default to linear
eased_component = value
# Blend between linear and eased
if exaggeration <= 0.0:
return value # Linear (no change)
elif exaggeration >= 1.0:
return eased_component # Full easing
else:
# Blend linearly between the two
return value * (1.0 - exaggeration) + eased_component * exaggeration
###
### SECTION 3: HERMITE EASING FUNCTION (CORRECTED)
###
def hermite_ease_out(value, strength=1.0):
"""
Apply an ease-out Hermite curve to a normalized value (0-1)
with adjustable strength parameter
Parameters:
value: Normalized value (0-1)
strength: Controls the shape of the curve (0.5-5.0 range)
Higher values make the curve steeper initially
Returns:
Eased value (0-1)
"""
# Ensure input is in valid range
value = max(0.0001, min(1.0, value)) # Avoid zero which could cause domain error
strength = max(0.51, min(5.0, strength)) # Prevent extreme values
# Modified Hermite ease-out formula with strength parameter
if abs(strength - 1.0) < 0.01:
# Standard Hermite: t * (2 - t)
return value * (2.0 - value)
elif strength < 1.0:
# Gentler curve for values < 1.0
return value ** (1.0/strength)
else:
# Sharper curve for values > 1.0
return 1.0 - ((1.0 - value) ** strength)
###
### SECTION 4: UTILITY FUNCTIONS
###
def normalize_value(value, min_val, max_val):
"""
Normalize a value to the range 0.0-1.0 based on min and max values
"""
range_val = max_val - min_val
epsilon = 1e-10
if range_val < epsilon:
return 0.0
clamped_value = max(min_val, min(max_val, value))
normalized = (clamped_value - min_val) / range_val
return normalized
def map_value(value, in_min, in_max, out_min, out_max):
"""
Map a value from one range to another
"""
# Normalize to 0-1 then scale to output range
normalized = normalize_value(value, in_min, in_max)
return out_min + normalized * (out_max - out_min)
###
### SECTION 5: GEOMETRIC HELPER FUNCTIONS
###
def angle_between_vectors(vec1, vec2):
"""
Calculate the angle in degrees between two vectors
"""
# Ensure vectors are unitized
if not vec1.IsUnitVector: vec1.Unitize()
if not vec2.IsUnitVector: vec2.Unitize()
# Calculate dot product and clamp to valid range
dot = max(-1.0, min(1.0, vec1 * vec2))
# Convert to degrees
angle_rad = math.acos(dot)
angle_deg = math.degrees(angle_rad)
return angle_deg
def get_tangent(curve, t):
"""
Helper function to get tangent with proper error handling
Returns a unitized tangent vector
"""
result = curve.TangentAt(t)
# Handle both return types (tuple or vector)
if isinstance(result, tuple):
success, tangent = result
if not success or tangent is None:
tangent = rg.Vector3d.ZAxis
else:
tangent = result
if tangent is None or tangent.IsZero:
tangent = rg.Vector3d.ZAxis
# Ensure the vector is unitized
if not tangent.IsUnitVector:
tangent.Unitize()
return tangent
###
### SECTION 6: CURVE PARAMETER GENERATION FUNCTIONS
###
def generate_constant_spacing_parameters(curve, spacing):
"""
Generate parameters along the curve with constant spacing
Parameters:
curve: The curve to analyze
spacing: The desired spacing between comb lines
Returns:
A list of parameters along the curve
"""
parameters = []
# Get curve domain and length
domain = curve.Domain
curve_length = curve.GetLength()
# Safety checks
if curve_length < 0.001:
return [domain.Mid]
# Ensure spacing is valid
spacing = max(0.0001, spacing)
# Calculate number of divisions
num_divisions = max(2, int(curve_length / spacing) + 1)
# Create evenly spaced parameters
for i in range(num_divisions):
t = float(i) / (num_divisions - 1)
# Use normalized length for uniform spacing
success, param = curve.NormalizedLengthParameter(t)
if not success:
# Fall back to linear interpolation
param = domain.Min + t * domain.Length
parameters.append(param)
# Make sure we have the end point
if parameters[-1] < domain.Max * 0.999:
parameters.append(domain.Max)
return parameters
### --------------------------------------------------------------
### SECTION 7: CURVATURE-BASED PARAMETER GENERATION (MODIFIED)
### --------------------------------------------------------------
def generate_curvature_based_parameters(curve, scan_resolution, min_spacing_factor, max_spacing_factor,
comb_bias=0.5, exaggeration_factor=0.0):
"""
Generate parameters along the curve based on curvature radius - MODIFIED
Parameters:
curve: The curve to analyze
scan_resolution: Resolution for initial curve scanning
min_spacing_factor: Factor for minimum spacing (percentage of curvature radius)
max_spacing_factor: Factor for maximum spacing (percentage of curvature radius)
comb_bias: Midpoint of the S-curve transition (0.1-0.9)
exaggeration_factor: Factor to enhance curvature contrast (0.0-1.0)
Returns:
A list of parameters along the curve
"""
parameters = []
# Get curve domain and length
domain = curve.Domain
curve_length = curve.GetLength()
# Safety checks
if curve_length < 0.001:
return [domain.Mid]
# Ensure inputs are valid
scan_resolution = max(0.0001, min(0.1, scan_resolution))
min_spacing_factor = max(0.001, min_spacing_factor)
max_spacing_factor = max(min_spacing_factor * 1.1, max_spacing_factor)
comb_bias = max(0.1, min(0.9, comb_bias)) # Ensure comb_bias is within valid range
exaggeration_factor = max(0.0, min(1.0, exaggeration_factor))
# Step 1: Scan the curve at an adaptive resolution to find min/max curvature
# PERFORMANCE: Adjust the scan step based on curve length
num_samples = max(20, min(200, int(curve_length / scan_resolution)))
# PERFORMANCE: Pre-allocate arrays
scan_params = [0.0] * num_samples
radii = [float('inf')] * num_samples
# Generate evenly spaced normalized parameters (faster than loop)
for i in range(num_samples):
t = float(i) / max(1, num_samples - 1)
# Convert to curve parameter
success, param = curve.NormalizedLengthParameter(t)
if not success:
param = domain.Min + t * domain.Length
scan_params[i] = param
# Evaluate curvature at each parameter
epsilon = 1e-10
# PERFORMANCE: Find min/max radius in a single pass
min_radius = float('inf')
max_radius = 0.0
for i, t in enumerate(scan_params):
# Get curvature (optimized)
result = curve.CurvatureAt(t)
cv = None
mag = 0.0
if isinstance(result, tuple):
success, cv = result
if success and cv is not None and cv.IsValid:
mag = cv.Length
else:
cv = result
if cv is not None and cv.IsValid:
mag = cv.Length
# Calculate radius
if mag > epsilon:
radius = 1.0 / mag
radii[i] = radius
# Update min/max in a single pass
if radius < min_radius:
min_radius = radius
if radius > max_radius and radius < 1e6: # Ignore extreme values
max_radius = radius
# Filter out infinite values and find actual min/max
finite_radii = [r for r in radii if r < 1e6 and r > epsilon]
if not finite_radii:
# Fallback if no finite radii found
print("WARNING: No finite curvature radii found, using constant spacing")
return generate_constant_spacing_parameters(curve, min_spacing_factor)
# PERFORMANCE: Use the pre-calculated min/max
if min_radius == float('inf') or max_radius == 0.0:
# Re-calculate in case the single-pass logic failed
finite_radii.sort()
min_radius = finite_radii[0]
max_radius = finite_radii[-1]
print("DEBUG: Min radius found:", min_radius)
print("DEBUG: Max radius found:", max_radius)
# Step 2: Begin at start of curve
current_param = domain.Min
parameters.append(current_param)
# Get initial point
current_point = curve.PointAt(current_param)
last_placed_point = current_point
last_placed_param = current_param
# Step 3: Walk along curve placing comb lines based on curvature radius
# Use a much finer step size for walking the curve to ensure proper spacing
# This is critical for proper spacing in high curvature regions
adaptive_steps = max(100, min(1000, int(curve_length / (min_spacing_factor * 0.25))))
# PERFORMANCE: Pre-calculate log values for mapping
log_min = math.log(max(epsilon, min_radius))
log_max = math.log(max(log_min + epsilon, max_radius))
log_range = log_max - log_min
# More fine-grained approach - use direct parameter stepping
# This approach ensures that we don't miss any significant curvature changes
step_size = domain.Length / adaptive_steps
# For tracking which distances we've already placed
# Step through curve with small steps
param = domain.Min # Start at beginning
while param <= domain.Max:
# Get current point
current_point = curve.PointAt(param)
# Calculate distance from last placed point
distance_moved = current_point.DistanceTo(last_placed_point)
# Get current curvature
result = curve.CurvatureAt(param)
cv = None
mag = 0.0
if isinstance(result, tuple):
success, cv = result
if success and cv is not None and cv.IsValid:
mag = cv.Length
else:
cv = result
if cv is not None and cv.IsValid:
mag = cv.Length
# Calculate radius (with safety check)
current_radius = float('inf')
if mag > epsilon:
current_radius = 1.0 / mag
# Calculate desired spacing at this point based on radius percentage
# MODIFIED: Calculate spacing as percentage of current radius
if current_radius < float('inf'):
min_spacing = min_spacing_factor * current_radius # e.g., 0.005 * 60mm = 0.3mm
max_spacing = max_spacing_factor * current_radius # e.g., 0.05 * 215mm = 10.75mm
# Calculate normalized position for S-curve based on log-scaled radius
if log_range > epsilon:
log_current = math.log(max(epsilon, current_radius))
# Normalize between 0-1 with 0 = tight curve, 1 = flat curve
normalized_radius = (log_current - log_min) / log_range
# Apply S-curve with comb_bias as midpoint (where output = 0.5)
# MODIFIED: Use comb_bias to center the S-curve
norm_from_bias = normalized_radius - comb_bias
sigmoid_value = 1.0 / (1.0 + math.exp(-norm_from_bias * 10))
# Apply easing to enhance contrast
if exaggeration_factor > 0:
sigmoid_value = apply_easing(sigmoid_value, exaggeration_factor, "hermite", HermiteStrength)
# Map to spacing range
desired_spacing = min_spacing + sigmoid_value * (max_spacing - min_spacing)
else:
# If range is too small, use average spacing
desired_spacing = (min_spacing + max_spacing) * 0.5
else:
# For infinite radius (straight line), use max spacing
desired_spacing = max_spacing_factor * 1000 # Arbitrary large value for straight lines
# Place a comb line if we've moved far enough
if distance_moved >= desired_spacing:
parameters.append(param)
# Update reference for next evaluation
last_placed_point = current_point
last_placed_param = param
# Fixed-size parameter step (small enough to catch tight curves)
param += step_size
# Safety exit for domain end
if param > domain.Max and last_placed_param < domain.Max * 0.999:
parameters.append(domain.Max)
break
# Ensure we have the end point
if parameters[-1] < domain.Max * 0.999:
parameters.append(domain.Max)
print(f"DEBUG: Generated {len(parameters)} parameters based on curvature radius")
# Debug: Check min actual spacing
if len(parameters) >= 2:
min_actual_spacing = float('inf')
prev_pt = curve.PointAt(parameters[0])
for i in range(1, len(parameters)):
curr_pt = curve.PointAt(parameters[i])
spacing = curr_pt.DistanceTo(prev_pt)
if spacing < min_actual_spacing:
min_actual_spacing = spacing
prev_pt = curr_pt
print(f"DEBUG: Minimum actual spacing achieved: {min_actual_spacing:.6f}")
return parameters
###
### SECTION 8: COLOR GENERATION FUNCTIONS
###
def generate_sampled_radii_colors(radii_list, min_radius, max_radius, palette, invert=False):
"""
Generate colors for the sampled radii points based on the palette
Parameters:
radii_list: List of radii values
min_radius: Minimum radius value
max_radius: Maximum radius value
palette: Color palette to use
invert: Whether to invert the mapping (radius to color)
Returns:
colors: List of colors for each radius value
"""
if not radii_list or not palette:
return []
colors = []
epsilon = 1e-10
num_colors = len(palette)
# Ensure valid min/max radii
if min_radius <= epsilon or max_radius <= epsilon or max_radius <= min_radius:
min_infinite = True
for r in radii_list:
if r < float('inf') and r > epsilon:
if min_infinite or r < min_radius:
min_radius = r
min_infinite = False
if r > max_radius:
max_radius = r
# Generate a color for each radius
for radius in radii_list:
if radius <= epsilon or radius >= float('inf'):
# Zero or infinite radius - use first or last color
color_index = 0 if invert else num_colors - 1
else:
# Normalize radius between min and max radius (logarithmic)
log_min = math.log(max(epsilon, min_radius))
log_max = math.log(max(log_min + epsilon, max_radius))
log_radius = math.log(max(epsilon, radius))
norm_radius = (log_radius - log_min) / (log_max - log_min)
norm_radius = max(0.0, min(1.0, norm_radius))
if invert:
norm_radius = 1.0 - norm_radius
# Map to color index
color_idx_float = norm_radius * (num_colors - 1)
color_index = int(round(color_idx_float))
color_index = max(0, min(num_colors - 1, color_index))
colors.append(palette[color_index])
return colors
### --------------------------------------------------------------
### SECTION 9: BOUNDARY LINE GENERATION (MODIFIED)
### --------------------------------------------------------------
def generate_smooth_boundary_lines(curve, end_points, boundary_line_sample, palette=None, use_palette=False,
curvature_mag_list=None, min_mag=0.0, max_mag=1.0,
min_length=10.0, max_length=50.0, added_length=10.0,
invert_length=False, exaggeration=0.0):
"""
Generate smooth boundary lines by sampling the original curve at a constant increment
and applying the same length calculation as the comb lines.
"""
print("DEBUG: Values used for boundary line:")
print(f" min_length = {min_length}")
print(f" max_length = {max_length}")
print(f" added_length = {added_length}")
print(f" invert_length = {invert_length}")
print(f" exaggeration = {exaggeration}")
print(f" boundary_line_sample = {boundary_line_sample}")
if not curve or not curve.IsValid:
return [], []
# Initialize output lists
boundary_lines = []
boundary_colors = []
# Sample the curve at a finer resolution for smooth boundary, now using BoundaryLineSample
boundary_params = generate_constant_spacing_parameters(curve, boundary_line_sample)
# Rest of the function remains the same...
# ... (existing code for boundary line generation)
# Calculate boundary points
boundary_points = []
boundary_mags = []
epsilon = 1e-10
for t in boundary_params:
# Get curve point and curvature
pt = curve.PointAt(t)
# Get curvature - handle both return patterns
result = curve.CurvatureAt(t)
if isinstance(result, tuple):
success, cv = result
if not success or cv is None or not cv.IsValid:
cv = rg.Vector3d.Zero
mag = 0.0
else:
mag = cv.Length
else:
cv = result
if cv is None or not cv.IsValid:
cv = rg.Vector3d.Zero
mag = 0.0
else:
mag = cv.Length
# Handle NaN or Inf values
if math.isnan(mag) or math.isinf(mag):
mag = 0.0
cv = rg.Vector3d.Zero
# Save magnitude for coloring
boundary_mags.append(mag)
# Calculate Line Length using same logic as comb lines
normalized_mag = normalize_value(mag, min_mag, max_mag)
# Apply hermite easing with the HermiteStrength parameter
exaggerated_norm_mag = apply_easing(normalized_mag, exaggeration, "hermite", HermiteStrength)
if invert_length:
# For inverted length, high curvature = shorter lines
exaggerated_norm_mag = 1.0 - exaggerated_norm_mag
# Map the normalized exaggerated value to the min-max length range
calculated_length = map_value(exaggerated_norm_mag, 0.0, 1.0, min_length, max_length)
# Apply the additional length
current_length = calculated_length + added_length
# Calculate boundary point position
scaled_vector = rg.Vector3d.Zero
direction_vector = rg.Vector3d.Zero
if mag > epsilon and cv is not None and not cv.IsZero and cv.IsValid:
temp_dir = rg.Vector3d(cv)
if temp_dir.Unitize():
direction_vector = temp_dir
scaled_vector = direction_vector * current_length # Use final adjusted length
# Offset point from curve along curvature vector
boundary_pt = pt - scaled_vector
boundary_points.append(boundary_pt)
# Create line segments between boundary points
if len(boundary_points) >= 2:
for i in range(len(boundary_points) - 1):
pt1 = boundary_points[i]
pt2 = boundary_points[i+1]
line = rg.Line(pt1, pt2)
boundary_lines.append(line)
# Determine color if palette is provided
if use_palette and palette:
# For color, use the magnitude at this point
mag = boundary_mags[i]
# Normalize and get color index
norm_mag = normalize_value(mag, min_mag, max_mag)
color_idx_float = norm_mag * (len(palette) - 1)
color_index = int(round(color_idx_float))
color_index = max(0, min(len(palette) - 1, color_index))
boundary_colors.append(palette[color_index])
print(f"DEBUG: Generated {len(boundary_lines)} boundary line segments")
return boundary_lines, boundary_colors
### --------------------------------------------------------------
### SECTION 10: INPUT PARAMETER HANDLING
### --------------------------------------------------------------
# --- Main Script ---
# --- Define Expected Inputs ---
# Start performance timing
start_time = time.time()
# Initialize error message list
error_messages = []
# Add new input for CombBias
try:
CombBias = float(CombBias) if 'CombBias' in globals() else 0.5
CombBias = max(0.1, min(0.9, CombBias)) # Clamp to valid range
except (ValueError, TypeError):
print("ERROR: CombBias could not be converted to float, using default 0.5")
CombBias = 0.5
# Debug printing
print("DEBUG: CombBias value =", CombBias)
# Validate CombBias parameter
if CombBias is None or CombBias < 0.1 or CombBias > 0.9:
error_messages.append("'CombBias' must be between 0.1 and 0.9. Using clamped value.")
if CombBias is not None:
CombBias = max(0.1, min(0.9, CombBias))
else:
CombBias = 0.5
# Add new input for BoundaryLineSample
try:
BoundaryLineSample = float(BoundaryLineSample) if 'BoundaryLineSample' in globals() else 2.0
except (ValueError, TypeError):
print("ERROR: BoundaryLineSample could not be converted to float, using default 2.0")
BoundaryLineSample = 2.0
# Debug printing for new parameter
print("DEBUG: BoundaryLineSample value =", BoundaryLineSample)
# Validate BoundaryLineSample parameter in validation section
if BoundaryLineSample is None or BoundaryLineSample <= 0:
error_messages.append("'BoundaryLineSample' must be > 0. Using default value of 2.0.")
BoundaryLineSample = 2.0
# Add new input for Hermite strength
try:
HermiteStrength = float(HermiteStrength) if 'HermiteStrength' in globals() else 1.0
except (ValueError, TypeError):
print("ERROR: HermiteStrength could not be converted to float, using default 1.0")
HermiteStrength = 1.0
# Validate HermiteStrength parameter
if HermiteStrength is None or HermiteStrength <= 0.0:
error_messages.append("'HermiteStrength' must be > 0. Using default value of 1.0.")
HermiteStrength = 1.0
# Add new input for ScanResolution
try:
ScanResolution = float(ScanResolution) if 'ScanResolution' in globals() else 0.25
except (ValueError, TypeError):
print("ERROR: ScanResolution could not be converted to float, using default 0.25")
ScanResolution = 0.25
# Add new input for minimum and maximum length
try:
MinLength = float(MinLength) if 'MinLength' in globals() else 10.0
except (ValueError, TypeError):
print("ERROR: MinLength could not be converted to float, using default 10.0")
MinLength = 10.0
try:
MaxLength = float(MaxLength) if 'MaxLength' in globals() else 50.0
except (ValueError, TypeError):
print("ERROR: MaxLength could not be converted to float, using default 50.0")
MaxLength = 50.0
# Add new input for exaggeration factor (0.0 = linear, 1.0 = exponential)
try:
Exaggeration = float(Exaggeration) if 'Exaggeration' in globals() else 0.0
Exaggeration = max(0.0, min(1.0, Exaggeration))
except (ValueError, TypeError):
print("ERROR: Exaggeration could not be converted to float, using default 0.0 (linear)")
Exaggeration = 0.0
# Add new input for proportional spacing toggle
try:
UseProportionalSpacing = bool(UseProportionalSpacing) if 'UseProportionalSpacing' in globals() else True
except (ValueError, TypeError):
print("ERROR: UseProportionalSpacing could not be converted to boolean, using default True")
UseProportionalSpacing = True
# The constant value added to line lengths
try:
AddedLength = float(AddedLength) if 'AddedLength' in globals() else 10.0
except (ValueError, TypeError):
print("ERROR: AddedLength could not be converted to float, using default 10.0")
AddedLength = 10.0
# Add new input for marker points matching toggle
try:
# Boolean to toggle whether marker points match comb line positions
MarkerPointsMatch = bool(MarkerPointsMatch) if 'MarkerPointsMatch' in globals() else False
except (ValueError, TypeError):
print("ERROR: MarkerPointsMatch could not be converted to boolean, using default False")
MarkerPointsMatch = False
try:
MaxSpacing = float(MaxSpacing) if 'MaxSpacing' in globals() else 20.0
except (ValueError, TypeError):
print("ERROR: MaxSpacing could not be converted to float, using default 20.0")
MaxSpacing = 20.0
try:
MinSpacing = float(MinSpacing) if 'MinSpacing' in globals() else 2.0
except (ValueError, TypeError):
print("ERROR: MinSpacing could not be converted to float, using default 2.0")
MinSpacing = 2.0
###
### SECTION 11: DEBUG OUTPUT AND INITIALIZATION
###
# Debug printing
print("DEBUG: ScanResolution value =", ScanResolution)
print("DEBUG: MinLength value =", MinLength)
print("DEBUG: MaxLength value =", MaxLength)
print("DEBUG: Exaggeration value =", Exaggeration)
print("DEBUG: AddedLength value =", AddedLength)
print("DEBUG: MinSpacing value =", MinSpacing)
print("DEBUG: MaxSpacing value =", MaxSpacing)
print("DEBUG: UseProportionalSpacing =", UseProportionalSpacing)
print("DEBUG: MarkerPointsMatch =", MarkerPointsMatch)
print("DEBUG: HermiteStrength value =", HermiteStrength)
# --- Initialize Output Variables ---
Lines = []; Colors = []; BoundaryLines = []; BoundaryColors = []
MinRadiusOutput = None; MaxRadiusOutput = None; SampledRadii = []; SampledMarkerPoints = []
CalculatedLengths = []; SampledRadiiColors = [] # Added SampledRadiiColors output