SuperComb, a supercharged Curvature rebuild

SuperComb, a Rebuild of Curvature Analysis with Grasshopper: Finer Controls, Enhanced Visualization

I’ve rewritten Rhino’s curvature analysis tool for my specific needs using Grasshopper and Python. I’ve taken advantage of Anthropic’s Claude for AI-assisted coding to expand what’s possible with this enhanced visualization method. The AI written GH Python coding was a rewarding experience. This took about 20-30 iterations of adding on features and testing.

The integer-only scaling limitation and fine line width of the Rhino native tool was my initial motivation in rebuilding this function. The standard tool forces values like 125 or 126 with no ability to input 125.5 - and the visual jump between consecutive integers is dramatic. This SuperComb implementation allows for much finer, smoother control over line length and display.


The color-mapping functionality adds another dimension entirely. The comb lines can now be related to customizable color palettes through two methods:

  1. Dynamic mapping: Adjusts the full color palette to the min/max values present in a curve.
  2. Fixed or referenced mapping: Assigns colors to specific radius thresholds defined by inputs.

One interesting discovery was the ability to flip the magnitude display. My first introduction, back in the 90s, I found it counterintuitive that tighter curvature (smaller radius) generates longer display lines. I adapted to this CAD convention quickly, but I’ve never viewed any one system as the gold standard - it’s about adapting tools to what works for my exploration process.

For work like helmet design, I’ve added toggleable radius labels showing millimeter measurements. The density of these labels can be adjusted from sparse reference points to comprehensive coverage. ( This inspired the physical 3D printed gauges in this post Radius Gauges, 3D Printed )

You can maintain the classic fine “comb” appearance or thicken the lines for a gradient effect that reveals broader curvature transitions across complex forms. The Human LineWeight node is used to accomplish this, if there is a new native Rhino function that will display line with weights I would be happy to incorporate it.

Probable Asked Questions

Q: How does the performance compare to the native tool?
A: No noticeable lag - visualization updates instantaneously even with multiple curves and live adjustment.

Q: Which Rhino versions are supported?
A: Developed in Rhino WIP 9. Haven’t tested extensively on 6/7/8 or Mac vs Windows - feedback welcome if you try it.

Q: Are there any plugin dependencies?
A: Requires Human plugin for line thickness visualization. Very open to a standard Rhino tool if it exists. Uses the standard Python 3 included with Grasshopper.

Q: Can the visualization be baked into the Rhino document?
A: Haven’t implemented this yet, no problem to do. Could be useful for documentation, not essential for my workflow.

Q: Does this work for surface analysis too?
A: Not yet - but surface analysis is in development. Looking to extend this to analyze surface curvature with similar visualization options.

Q: How do I get curves into the system?
A: Works with any curve objects fed into Grasshopper.

Q: Can I export the curvature data?
A: Would be possible. The curvature data can be exported to Excel/CSV along with points for further analysis.

Q: Can I compare multiple curves simultaneously?
A: Yes. Multiple curves can be analyzed simultaneously. When using dynamic mapping, each curve uses the full color spectrum independently using the ranges contained in the given curve. With fixed mapping, colors represent consistent radius values across all curves.

Q: What’s the user interface like?
A: While the attached example uses standard GH sliders, my workflow utilizes OSC interface with a touch screen for more fluid control and tracking of the parameters.

Q: How does it handle very complex curves?
A: Haven’t tested on extremely complex curves yet - feedback welcome from anyone who pushes these boundaries.

Q: Will this slow down my system?
A: Minimal footprint - everything runs instantaneously on standard hardware.

I’m sharing the full code and a diagram of all inputs with descriptions of variables. Feel free to download, use, and extend it for your own workflow. I’d appreciate feedback, especially if you find interesting applications beyond what I’ve outlined or additions to the visualizations.

Supercomb.ghx (417.1 KB)

12 Likes

SuperComb: Updates to Curvature Analysis Script for Grasshopper

I’ve made/requested/summoned some updates to my “SuperCombs” script for visualizing curve curvature in Grasshopper. It’s basically an enhanced version of the standard Rhino curvature visualization, giving you more control options and better visual output. Main additions in this update are proportional spacing and fixed minimum and maximum comb line display.

What’s Updated

Better Curvature-Based Density Distribution


Previously, the constant spacing of the comb lines result in sparse in areas with the smallest curvature radius (tight corners), and in blocky display, or required oversampling the entire curve. Now the density of comb lines adjusts based on curvature - tighter curves get more definition with closer spacing, while gentle curves have wider spacing. This helps avoid that “spiky” look around corners that used to force you to increase resolution everywhere.

Dual Spacing Modes with Percentage-Based Approach

I’ve implemented two spacing modes that you can toggle between:

The proportional approach uses factors of curvature radius opposed to fixed spacing along the curve. The spacing adjusts proportionally to each curve segment:

  • MinSpacing (0.005) on a 60mm radius curve gives you 0.3mm spacing
  • MaxSpacing (0.05) on a 215mm radius curve gives you 10.75mm spacing

The toggle lets you switch between these modes depending on your visualization needs.

New CombBias Parameter

There’s a new CombBias parameter (ranges from 0.1-0.9) that controls how the transition between minimum and maximum spacing gets distributed. This helps when you have peaks of maximum curvature that might otherwise dominate the density of the comb line visualization.

Separate BoundaryLineSample Parameter

The boundary line (that extended outer line connecting the ends of comb lines) now has its own sampling parameter. Before, if you used sparse comb sampling, you’d get a blocky, straight-line boundary, in a single segmented single color that just didn’t look great. Now you can have a smooth boundary curve even with sparse comb density. It’s mainly a visual thing, not really affecting the analysis functionality, but makes me feel better to use it. This setting has the biggest effect on performance.

S-Curve Transition

The script now uses an S-curve with adjustable midpoint for transitions between min and max values, which creates more natural-looking transitions between different curvature zones.

Performance

The script runs between 180-200ms to a couple seconds, mostly depending on boundary curve sampling density.

When You Might Use This

This tends to be useful when you need to:

  • Get more precise curve analysis than the built-in tools
  • Adjust visualization to highlight specific curvature ranges
  • Create cleaner curvature visualizations for presentations

A Note on AI-Summoned Development

While I feel the same way about using “AI” in a sentence as “NFT” it seems tired and buzzed out, the functionality and awareness of the Grasshopper environment and writing Python code is quite exceptional. This was not a project that I could have attempted on my own.

This script is over 1,000 lines of well-documented Python code, which was developed with/by Claude by Anthropic. This project required substantial specification work and went through probably 20-30 iterations as ideas were executed and new possibilities discovered. Just a statement of fact, not congratulating myself on hard work, the engine does all the code work to hit the briefed goals.

The process revealed interesting possibilities. For instance, I started with various iterations of cosine-based distribution along curves before discovering that a Hermite ease-out ( learned about this during the course )function worked better for spreading the density of comb lines across the display.

When working with Claude, I found it runs into some limitations around 700 lines of code, which is why the script is now split into discrete, well-labeled sections.

This approach has opened up new possibilities for me in implementing custom Rhino functionality with Grasshopper. The curvature visualization is just one example of what’s possible with this development method.


Annotated radial dimension dots, constant spacing, sparse.

Annotated radial dimension dots, constant spacing, dense.

Annotations matching comb line placement, coase resolution in tight radius curvature.

Annotations matching comb line placement, Sparse in larger radius curvature, more dense in smaller radius curvature.

Supercomb.ghx (524.6 KB)

2 Likes

I cannot find/search it in R8.

This is a custom Python3 Function, in the attached .ghx file.

# Full Python script for Advanced Grasshopper Curvature Comb
# VERSION WITH Min/Max Length + Cosine Exaggeration + Curvature Radius-Based Distribution + Refined Boundary
# MODIFIED: Radius-based length easing + PascalCase consistency fix

SuperCombs: Advanced Grasshopper Curvature Comb Script - Technical Reference

Overview

This Python script enhances curve curvature visualization in Grasshopper with expanded control over display parameters. It generates comb lines perpendicular to the curve based on curvature values, with adjustable density, length, and coloring options.

Input Parameters

Primary Controls

  • crv: Input curve to analyze
  • UseProportionalSpacing (boolean): Toggles between curvature-based spacing and constant spacing
  • MinSpacing (float): Factor for minimum spacing as percentage of curvature radius (e.g., 0.005)
  • MaxSpacing (float): Factor for maximum spacing as percentage of curvature radius (e.g., 0.05)
  • MinLength (float): Minimum comb line length (default: 10.0)
  • MaxLength (float): Maximum comb line length (default: 50.0)
  • AddedLength (float): Constant length added to all comb lines (default: 10.0)
  • InvertLength (boolean): When true, inverts comb line length calculation

Advanced Controls

  • CombBias (float, 0.1-0.9): Controls the midpoint of S-curve transition between min/max spacing
  • BoundaryLineSample (float): Controls sample resolution for boundary line generation
  • HermiteStrength (float, 0.51-5.0): Controls curve steepness for Hermite easing
  • Exaggeration (float, 0.0-1.0): Factor for enhancing curvature contrast
  • MarkerPointsMatch (boolean): When true, marker points match comb line positions
  • NumSections: Number of sections for marker point generation
  • ScanResolution (float): Resolution for curve scanning

Color Controls

  • palette: Optional color palette for coloring comb lines
  • UseRadiusRange (boolean): When true, uses custom radius range for coloring
  • MinRadiusInput: Minimum radius value for color mapping
  • MaxRadiusInput: Maximum radius value for color mapping

Script Flow

The script processes the input curve through several sequential stages:

  1. Parameter Validation (Section 10-12)

    • Validates all input parameters
    • Sets default values for missing or invalid inputs
    • Initializes output variables
  2. Parameter Generation (Section 13)

    • Converts curve to NURBS for consistent handling
    • Generates parameters along curve based on specified spacing method
    • If UseProportionalSpacing is true, uses curvature-based parameters
    • If false, uses constant spacing with MaxSpacing*100 value
  3. Point Evaluation (Section 13)

    • Evaluates curve points at each parameter
    • Calculates curvature magnitude at each point
    • Determines min/max curvature values for scaling
  4. Marker Point Calculation (Section 14)

    • Generates marker points based on curvature data
    • Positions marker points based on calculated comb line lengths
    • If MarkerPointsMatch is true, matches marker points to comb lines
    • Otherwise, distributes them evenly along the curve
  5. Comb Line Generation (Section 15)

    • Creates perpendicular lines at each parameter point
    • Calculates line length based on normalized curvature magnitude
    • Applies easing and scaling to line lengths
    • Assigns colors based on curvature values if palette is provided
  6. Boundary Line Generation (Section 16)

    • Creates a smooth boundary line connecting comb line endpoints
    • Uses BoundaryLineSample for boundary resolution
    • Maintains consistent styling with comb lines
  7. Output Variable Assignment (Section 17)

    • Populates output variables with generated geometry
    • Provides debug summary information
    • Reports execution time

Key Functions

def apply_easing(value, exaggeration, easing_type="cosine", strength=1.0):
    # Applies easing functions to create smooth transitions between values
def hermite_ease_out(value, strength=1.0):
    # Implements adjustable Hermite ease-out curve for natural transitions
def generate_constant_spacing_parameters(curve, spacing):
    # Creates evenly spaced parameters along the curve for constant sampling
def generate_curvature_based_parameters(curve, scan_resolution, min_spacing_factor, 
                                       max_spacing_factor, comb_bias=0.5, exaggeration_factor=0.0):
    # Creates parameters with spacing based on curvature radius
    # Tighter curves get more detail with closer spacing
def generate_smooth_boundary_lines(curve, end_points, boundary_line_sample, ...):
    # Creates a smooth boundary connecting comb line endpoints

Output Parameters

  • Lines: List of line objects representing the comb lines
  • Colors: List of colors for the comb lines (if palette provided)
  • BoundaryLines: List of line objects forming the boundary curve
  • BoundaryColors: List of colors for the boundary lines (if palette provided)
  • SampledRadii: List of radius values at sampled points
  • SampledMarkerPoints: List of marker point positions
  • SampledRadiiColors: List of colors for radius visualization
  • MinRadiusOutput: Minimum radius found in the curve
  • MaxRadiusOutput: Maximum radius found in the curve

Performance

The script typically executes in 180-200 milliseconds for most curves. Performance optimizations include:

  • Pre-allocation of arrays
  • Single-pass processing where possible
  • Logarithmic scaling for better distribution of values
  • Adaptive step sizes based on curvature
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


Expand to view remaining Sections 12 - 17
###
###    SECTION 12: INPUT VALIDATION
###

# --- Validate Inputs ---
inputs_valid = True; epsilon = 1e-10
if crv is None or not crv.IsValid: error_messages.append("'crv' is null or invalid."); inputs_valid = False
if palette is None or len(palette) == 0: use_palette = False; num_colors = 0
else: use_palette = True; num_colors = len(palette)
valid_num_sections = True
if NumSections is None or NumSections < 1: error_messages.append("'NumSections' must be >= 1. Skipping sampling."); valid_num_sections = False
if InvertLength is None: InvertLength = False
if UseRadiusRange is None: UseRadiusRange = False

# Validate length parameters
if MinLength is None or MinLength < 0: 
    error_messages.append("'MinLength' must be >= 0. Using default value of 10.0.")
    MinLength = 10.0
if MaxLength is None or MaxLength <= MinLength:
    error_messages.append("'MaxLength' must be > MinLength. Using default value of " + str(MinLength + 40.0) + ".")
    MaxLength = MinLength + 40.0

# Validate Exaggeration parameter
if Exaggeration is None or Exaggeration < 0.0 or Exaggeration > 1.0:
    error_messages.append("'Exaggeration' must be between 0.0 and 1.0. Using clamped value.")
    if Exaggeration is not None:
        Exaggeration = max(0.0, min(1.0, Exaggeration))
    else:
        Exaggeration = 0.0

# Validate ScanResolution parameter
if ScanResolution is None or ScanResolution <= 0:
    error_messages.append("'ScanResolution' must be > 0. Using default value of 0.25.")
    ScanResolution = 0.25

# Check AddedLength
if AddedLength is None or AddedLength < 0:
    error_messages.append("'AddedLength' must be >= 0. Using default value of 10.0.")
    AddedLength = 10.0

# Validate angular change parameters
if MaxSpacing is None or MaxSpacing <= 0:
    error_messages.append("'MaxSpacing' must be > 0. Using default value of 20.0.")
    MaxSpacing = 20.0
if MinSpacing is None or MinSpacing < 0:
    error_messages.append("'MinSpacing' must be >= 0. Using default value of 2.0.")
    MinSpacing = 2.0
if MinSpacing >= MaxSpacing:
    error_messages.append("'MinSpacing' must be < 'MaxSpacing'. Adjusting values.")
    MinSpacing = min(MinSpacing, MaxSpacing / 2.0)

radius_mode_active_and_valid = False
if inputs_valid and UseRadiusRange:
    if MinRadiusInput is None or MaxRadiusInput is None or MinRadiusInput < 0 or MaxRadiusInput <= MinRadiusInput:
        error_messages.append("Warning: Invalid Min/Max Radius inputs. Reverting color mode.")
        pass
    else: radius_mode_active_and_valid = True

if error_messages:
    for msg in error_messages: print(msg)

# --- Main Execution Block ---
if not inputs_valid:
    print("Script execution halted due to invalid inputs.")
    pass
else:
    # Report time to input validation
    validation_time = time.time() - start_time
    print("Input validation time: {:.3f} seconds".format(validation_time))
    
    nc = crv.ToNurbsCurve()
    if nc is None: print("Warning: Using original curve type."); nc = crv
    domain = nc.Domain

    # --- Generate Parameters based on spacing type ---
    params_start_time = time.time()
    
    if UseProportionalSpacing:
        # Use curvature-based spacing instead of angular change
        # Now passing MinSpacing and MaxSpacing as factors, along with CombBias
        parameters = generate_curvature_based_parameters(nc, ScanResolution, MinSpacing, MaxSpacing, CombBias, Exaggeration)
        print("DEBUG: Generated", len(parameters), "parameters based on curvature radius")
    else:
        # Use constant spacing
        parameters = generate_constant_spacing_parameters(nc, MaxSpacing*100)
        print("DEBUG: Generated", len(parameters), "parameters with constant spacing")

    params_time = time.time() - params_start_time
    print("Parameter generation time: {:.3f} seconds".format(params_time))

    # --- Evaluate Points & Calculate Min/Max ---
    eval_start_time = time.time()
    
    min_mag_actual = 0.0; max_mag_actual = 0.0; min_mag_non_zero_actual = float('inf')
    min_radius_actual = float('inf'); max_radius_actual = float('inf')
    points_comb = []; curvatures_comb = []; curvature_magnitudes_comb = []
    
    if parameters:
        # PERFORMANCE: Pre-allocate arrays
        num_params = len(parameters)
        points_comb = [None] * num_params
        curvatures_comb = [None] * num_params
        curvature_magnitudes_comb = [None] * num_params
        
        for i, t in enumerate(parameters):
            t_clamped = max(domain.Min, min(domain.Max, t))
            
            # Getting point and curvature in one pass
            pt = nc.PointAt(t_clamped)
            points_comb[i] = pt
            
            # Get curvature - handle both return patterns
            result = nc.CurvatureAt(t_clamped)
            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
                
            if math.isnan(mag) or math.isinf(mag):
                mag = 0.0
                cv = rg.Vector3d.Zero
                
            curvatures_comb[i] = cv
            curvature_magnitudes_comb[i] = mag
            
        # Calculate min/max values in one pass
        if curvature_magnitudes_comb:
            min_mag_actual = min(curvature_magnitudes_comb)
            max_mag_actual = max(curvature_magnitudes_comb)
            positive_mags = [m for m in curvature_magnitudes_comb if m > epsilon]
            if positive_mags: min_mag_non_zero_actual = min(positive_mags)
        
        if max_mag_actual > epsilon: min_radius_actual = 1.0 / max_mag_actual
        if min_mag_non_zero_actual != float('inf'): max_radius_actual = 1.0 / min_mag_non_zero_actual
        
    MinRadiusOutput = min_radius_actual
    MaxRadiusOutput = max_radius_actual
    
    eval_time = time.time() - eval_start_time
    print("Point evaluation time: {:.3f} seconds".format(eval_time))


###
###   SECTION 14: MARKER POINTS CALCULATION
###

# --- Section 4 - Calculate Sampled Radii & Marker Points ---
    sample_start_time = time.time()
    
    sampled_radii_list = []; sampled_marker_points_list = []
    
    if valid_num_sections:
        # Decide how to generate the sample points
        if MarkerPointsMatch and parameters and len(points_comb) > 0:
            # Use the comb line points for marker points
            num_points = len(points_comb)
            sampled_radii_list = [None] * num_points
            sampled_marker_points_list = [None] * num_points
            
            for i in range(num_points):
                # Get the comb line point and curvature data
                pt = points_comb[i]
                cv = curvatures_comb[i]
                mag = curvature_magnitudes_comb[i]
                
                # Calculate radius for output
                radius_at_point = float('inf')
                if mag > epsilon: radius_at_point = 1.0 / mag
                sampled_radii_list[i] = radius_at_point
                
                # Calculate length for marker point
                normalized_mag = normalize_value(mag, min_mag_actual, max_mag_actual)
                
                # Apply hermite easing to normalized magnitude with the HermiteStrength parameter
                exaggerated_norm_mag = apply_easing(normalized_mag, Exaggeration, "hermite", HermiteStrength)
                
                if InvertLength:
                    # 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, MinLength, MaxLength)
                        
                # Add the additional length
                current_length_sample = calculated_length + AddedLength
                
                # Calculate marker point geometry using this adjusted length
                scaled_vector_sample = rg.Vector3d.Zero
                direction_vector_sample = rg.Vector3d.Zero
                
                if mag > epsilon and cv is not None and cv.IsValid and not cv.IsZero:
                    temp_dir = rg.Vector3d(cv)
                    if temp_dir.Unitize():
                        direction_vector_sample = temp_dir
                        scaled_vector_sample = direction_vector_sample * current_length_sample
                        
                comb_end_pt_sample = pt - scaled_vector_sample
                outward_direction_sample = -direction_vector_sample
                
                marker_pt = pt  # Default
                if not outward_direction_sample.IsZero:
                    marker_pt = comb_end_pt_sample + (outward_direction_sample * 12.0)
                    
                sampled_marker_points_list[i] = marker_pt
                
        else:
            # Use uniform spacing along the curve for marker points
            num_points_to_sample = NumSections + 1
            sampled_radii_list = [None] * num_points_to_sample
            sampled_marker_points_list = [None] * num_points_to_sample
            
            for i in range(num_points_to_sample):
                norm_L = float(i) / NumSections
                norm_L = max(0.0, min(1.0, norm_L))
                
                # Get parameter at normalized length
                success, t = nc.NormalizedLengthParameter(norm_L)
                if not success: t = domain.Min + domain.Length * norm_L
                
                t_clamped = max(domain.Min, min(domain.Max, t))
                pt = nc.PointAt(t_clamped)
                
                # Get curvature - handle both return patterns
                result = nc.CurvatureAt(t_clamped)
                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
                    
                if math.isnan(mag) or math.isinf(mag):
                    mag = 0.0
                    cv = rg.Vector3d.Zero

                # Calculate radius for output
                radius_at_point = float('inf')
                if mag > epsilon: radius_at_point = 1.0 / mag
                sampled_radii_list[i] = radius_at_point

                # Calculate length for marker point
                normalized_mag_sample = normalize_value(mag, min_mag_actual, max_mag_actual)
                
                # Apply hermite easing to normalized magnitude with the HermiteStrength parameter
                exaggerated_norm_mag = apply_easing(normalized_mag_sample, Exaggeration, "hermite", HermiteStrength)
                
                if InvertLength:
                    # 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, MinLength, MaxLength)
                        
                # Add the additional length
                current_length_sample = calculated_length + AddedLength
                
                # Calculate marker point geometry using this adjusted length
                scaled_vector_sample = rg.Vector3d.Zero
                direction_vector_sample = rg.Vector3d.Zero
                
                if mag > epsilon and cv is not None and cv.IsValid and not cv.IsZero:
                    temp_dir = rg.Vector3d(cv)
                    if temp_dir.Unitize():
                        direction_vector_sample = temp_dir
                        scaled_vector_sample = direction_vector_sample * current_length_sample
                        
                comb_end_pt_sample = pt - scaled_vector_sample
                outward_direction_sample = -direction_vector_sample
                
                marker_pt = pt  # Default
                if not outward_direction_sample.IsZero:
                    marker_pt = comb_end_pt_sample + (outward_direction_sample * 12.0)
                    
                sampled_marker_points_list[i] = marker_pt
    
    # Generate colors for sampled radii
    if valid_num_sections and use_palette and sampled_radii_list:
        # Set SampledRadiiColors - new output
        SampledRadiiColors = generate_sampled_radii_colors(
            sampled_radii_list,       # List of radii values
            min_radius_actual,        # Min radius 
            max_radius_actual,        # Max radius
            palette,                  # Color palette
            False                     # Don't invert (small radius = high curvature = high color index)
        )
                
    SampledRadii = sampled_radii_list
    SampledMarkerPoints = sampled_marker_points_list
    
    sample_time = time.time() - sample_start_time
    print("Sampling time: {:.3f} seconds".format(sample_time))
    if MarkerPointsMatch:
        print("Used {} matched marker points".format(len(sampled_marker_points_list)))
    else:
        print("Used {} uniform marker points".format(len(sampled_marker_points_list)))


###
###   SECTION 15: COMB LINE GENERATION
###

# --- Section 5: Create Comb Lines, Colors, Endpoints ---
    lines_start_time = time.time()
    
    comb_lines_temp = []; comb_colors_temp = []; end_points_temp = []
    
    if parameters and len(points_comb) > 0:
        # Pre-allocate arrays for better performance
        num_points = len(points_comb)
        comb_lines_temp = [None] * num_points
        end_points_temp = [None] * num_points
        if use_palette: comb_colors_temp = [None] * num_points
        
        for i in range(num_points):
            pt = points_comb[i]
            cv = curvatures_comb[i]
            mag = curvature_magnitudes_comb[i]

            # Calculate Line Length
            normalized_mag = normalize_value(mag, min_mag_actual, max_mag_actual)
            
            # Apply hermite easing to normalized magnitude with the HermiteStrength parameter
            exaggerated_norm_mag = apply_easing(normalized_mag, Exaggeration, "hermite", HermiteStrength)
            
            if InvertLength:
                # 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, MinLength, MaxLength)
            
            # Store for debugging
            CalculatedLengths.append(calculated_length)
            
            # Apply the additional length
            current_length = calculated_length + AddedLength
            
            # Calculate Line Geometry using the final 'current_length'
            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
                     
            end_pt = pt - scaled_vector
            end_points_temp[i] = end_pt
            
            line = rg.Line(pt, end_pt)
            comb_lines_temp[i] = line

            # Determine Color
            if use_palette and num_colors > 0:
                color_norm_value = 0.0
                
                if radius_mode_active_and_valid:
                    current_radius = float('inf')
                    if mag > epsilon: current_radius = 1.0 / mag
                    radius_range_input = MaxRadiusInput - MinRadiusInput
                    norm_R = (current_radius - MinRadiusInput) / radius_range_input
                    color_norm_value = max(0.0, min(1.0, norm_R))
                else:
                    color_norm_value = normalized_mag
                    
                color_idx_float = color_norm_value * (num_colors - 1)
                color_index = int(round(color_idx_float))
                color_index = max(0, min(num_colors - 1, color_index))
                comb_colors_temp[i] = palette[color_index]

        Lines = comb_lines_temp
        Colors = comb_colors_temp
    

### --------------------------------------------------------------
###    SECTION 16: BOUNDARY LINE GENERATION (UPDATED)
### --------------------------------------------------------------

# --- Section 6: Create Refined Boundary Line Segments ---
        # Use new function to create refined boundary lines
        if len(end_points_temp) >= 2:
            boundary_lines_temp, boundary_colors_temp = generate_smooth_boundary_lines(
                nc,                         # The curve 
                end_points_temp,            # Endpoints of comb lines
                BoundaryLineSample,         # UPDATED: Now using separate BoundaryLineSample
                palette,                    # Color palette
                use_palette,                # Whether to use palette
                curvature_magnitudes_comb,  # For color interpolation
                min_mag_actual,             # Min curvature
                max_mag_actual,             # Max curvature
                MinLength,                  # Min length for calculation
                MaxLength,                  # Max length for calculation
                AddedLength,                # Added length
                InvertLength,               # Whether to invert length
                Exaggeration                # Exaggeration factor
            )
            
            BoundaryLines = boundary_lines_temp
            BoundaryColors = boundary_colors_temp

###
###    SECTION 17: FINAL SUMMARY AND OUTPUT
###

# Add final debug summary
print("DEBUG SUMMARY:")
print("  ScanResolution parameter =", ScanResolution)
print("  MinLength parameter =", MinLength)
print("  MaxLength parameter =", MaxLength)
print("  Exaggeration parameter =", Exaggeration)
print("  AddedLength parameter =", AddedLength)
print("  MinSpacing =", MinSpacing)
print("  MaxSpacing =", MaxSpacing)
print("  UseProportionalSpacing =", UseProportionalSpacing)
print("  MarkerPointsMatch =", MarkerPointsMatch)
print("  Generated comb lines:", len(Lines))
print("  Generated boundary lines:", len(BoundaryLines))
print("  Generated sampled radii colors:", len(SampledRadiiColors))
print("  HermiteStrength parameter =", HermiteStrength)
if CalculatedLengths:
    print("  Min calculated length:", min(CalculatedLengths))
    print("  Max calculated length:", max(CalculatedLengths))

# Report execution time
execution_time = time.time() - start_time
print("  Total execution time: {:.3f} seconds".format(execution_time))

# --- Final Check/Assignment (Safety net) ---
if 'Lines' not in locals(): Lines = []
if 'Colors' not in locals(): Colors = []
if 'BoundaryLines' not in locals(): BoundaryLines = []
if 'BoundaryColors' not in locals(): BoundaryColors = []
if 'MinRadiusOutput' not in locals(): MinRadiusOutput = None
if 'MaxRadiusOutput' not in locals(): MaxRadiusOutput = None
if 'SampledRadii' not in locals(): SampledRadii = []
if 'SampledMarkerPoints' not in locals(): SampledMarkerPoints = []
if 'SampledRadiiColors' not in locals(): SampledRadiiColors = []