New Chat
Ctrl
K
  • Websites
  • Docs
  • Slides
  • Sheets
  • Deep Research
Kimi Code Kimi Claw
Chat History
Mobile App
  • About Us
    • Visit Moonshot AI
    • Open Platform
    • Features
    • Terms of Service
    • Privacy Policy
  • Language
  • User Feedback

GHPython Script for Continuous Zig-Zag Polyline from Rotated Lines

I do need a GHPython script for Rhinoceros v7 that can take a number of horizontal lines and will create segments so after joining all the segment lines with the horizontal lines to have a continuous zig-zag curve with only one start and one end. I do need this to 3D print some filler shape and to minimize the travel of the printing head I do need to create this kind of shape. The shape is sliced in horizontal lines paralels to each other.
Search
GHPython script for Rhino 7 to generate continuous zig-zag curve from horizontal segments
20 results
Below is a ready-to-drop GHPython component for Grasshopper (Rhino 7) that converts any set of horizontal, parallel lines into one single, continuous, zig-zag polyline.
The idea is very simple:
  1. Sort the lines from bottom to top (by Y-coordinate of their start point).
  2. Walk through the sorted list, alternating the direction of travel (left-to-right, right-to-left, left-to-right …).
  3. Add the two endpoints of every line in that order to one big point list.
  4. Create one polyline from the list – the result is a single curve that has exactly one start and one end and never leaves the printing layer.
Copy the code into a GHPython component, feed the horizontal lines into the input Lines, and the component outputs the finished zig-zag polyline.
Python
Copy
"""
GHPython – continuous zig-zag polyline from horizontal slices
-------------------------------------------------------------
Input:
    Lines  - List[Curve]  – the horizontal slice curves (any length/position)
    flip   - bool        – optional toggle to reverse every second line (default = True)

Output:
    ZigZag - Curve       – one single polyline (start→end→start→end…)
"""

import rhinoscriptsyntax as rs
import Rhino.Geometry as rg

def create_zigzag(lines, flip_direction=True):
    if not lines:
        return None
    
    # 1.  Sort the lines from bottom to top (by Y of first control point)
    sorted_lines = sorted(lines,
                          key=lambda ln: rs.CurveStartPoint(ln).Y)
    
    pts = []
    
    # 2.  Walk through the lines, alternating direction
    for i, ln in enumerate(sorted_lines):
        pts_line = rs.DivideCurve(ln, 1)   # returns [start, end]
        
        # every second line is travelled backwards
        if flip_direction and (i % 2):
            pts_line.reverse()
        
        # skip the first point on every line except the very first one
        if i == 0:
            pts.extend(pts_line)
        else:
            pts.extend(pts_line[1:])
    
    # 3.  Build one single polyline
    zigzag = rs.AddPolyline(pts)
    return zigzag

# ---------------------------------------------
# Grasshopper entry point
if Lines:
    ZigZag = create_zigzag(Lines, flip_direction=flip if 'flip' in locals() else True)
else:
    ZigZag = None
How to use
  1. In Grasshopper, add a GHPython component.
  2. Paste the script above.
  3. Add an input called Lines (type hint: Curve, list access).
  4. (Optional) add a boolean input named flip to quickly reverse direction when needed.
  5. The output ZigZag is the single, continuous curve you can bake and send to the slicer – no travel moves between the original horizontal lines.
The script should create curves between the most closed starts and ends. Right not it is connecting the furthest points (starts and ends). I do need the less head travel as possible.
Here is a revised GHPython component that builds the shortest possible zig-zag.
Instead of blindly alternating “start→end / end→start”, it always connects the line that is closest to the current end point (and decides on-the-fly which of its two ends is nearer).
The result is the minimum travelling distance for the print head while still producing one single, continuous polyline.
Python
Copy
"""
GHPython – shortest-travel zig-zag from horizontal slices
---------------------------------------------------------
Input:
    Lines  – List[Curve] – the horizontal slice curves
Output:
    ZigZag – Curve       – one single polyline with minimum travel
"""

import Rhino.Geometry as rg

def closest_end(line_pts, current):
    """return the ordered pair (A,B) of the line's two ends so that
       A is the closer end to <current>, and B the other one."""
    s, e = line_pts
    return (s, e) if current.DistanceTo(s) < current.DistanceTo(e) else (e, s)

def build_minimum_zigzag(lines):
    if not lines:
        return None
    
    # 1.  Sort by Y (bottom → top)
    lines_sorted = sorted(lines, key=lambda ln: ln.PointAtStart.Y)
    
    # 2.  Pre-extract the two endpoints of every line
    line_ends = [(ln.PointAtStart, ln.PointAtEnd) for ln in lines_sorted]
    
    pts = [line_ends[0][0]]          # start at the bottom-left end
    remaining = set(range(1, len(line_ends)))
    current   = line_ends[0][0]
    
    # 3.  Greedy nearest-end walk
    while remaining:
        best_i   = None
        best_A   = None
        best_B   = None
        best_d   = 1e99
        
        for idx in remaining:
            A, B = closest_end(line_ends[idx], current)
            d    = current.DistanceTo(A)
            if d < best_d:
                best_d, best_i, best_A, best_B = d, idx, A, B
        
        # append the chosen line
        pts.append(best_A)
        pts.append(best_B)
        current = best_B
        remaining.remove(best_i)
    
    # 4.  Build the polyline
    poly = rg.Polyline(pts)
    # (optionally simplify to tolerance 1e-3 if you want)
    return poly.ToPolylineCurve()

# -------------------------------------------------
if Lines:
    crv = build_minimum_zigzag(Lines)
    ZigZag = crv
else:
    ZigZag = None
Wire it exactly as before:
Lines input → list of horizontal curves, ZigZag output → the single, shortest-travel zig-zag.
Works better, however, looks like it is skipping the last curve in the list which in this case is right after the first one!
The “last curve is skipped” happens because after the first line we add two points (A, B), and the greedy search starts looking for the next closest line from B – that can make it jump over the very next horizontal line if its nearest end is further away than some other line.
To guarantee that every horizontal line is fully traversed (start-to-end or end-to-start), we must explicitly add the two points of the current line before we move on to the next one.
Below is a minimal fix that does exactly this:
Python
Copy
"""
GHPython – shortest-travel zig-zag (guarantees every line is used)
------------------------------------------------------------------
Input:
    Lines  – List[Curve] – the horizontal slice curves
Output:
    ZigZag – Curve       – one single polyline with minimum travel
"""

import Rhino.Geometry as rg

def build_minimum_zigzag(lines):
    if not lines:
        return None
    
    # 1.  Sort by Y (bottom → top)
    lines_sorted = sorted(lines, key=lambda ln: ln.PointAtStart.Y)
    
    # 2.  Pre-extract endpoints
    line_ends = [(ln.PointAtStart, ln.PointAtEnd) for ln in lines_sorted]
    
    pts = []
    
    # 3.  Greedy nearest-end walk
    remaining = list(range(len(line_ends)))
    current   = line_ends[0][0]          # arbitrary start
    pts.append(current)
    
    while remaining:
        # find the closest **unused** line
        best_i = None
        best_A = None
        best_B = None
        best_d = float('inf')
        
        for idx in remaining:
            s, e = line_ends[idx]
            ds, de = current.DistanceTo(s), current.DistanceTo(e)
            if min(ds, de) < best_d:
                best_d = min(ds, de)
                best_i = idx
                if ds <= de:
                    best_A, best_B = s, e
                else:
                    best_A, best_B = e, s
        
        # add this line completely (A → B)
        if best_A != current:
            pts.append(best_A)
        pts.append(best_B)
        
        # remove line from pool and continue from its far end
        remaining.remove(best_i)
        current = best_B
    
    # 4.  Build the polyline
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
if Lines:
    ZigZag = build_minimum_zigzag(Lines)
else:
    ZigZag = None
Drop it into the same GHPython component and every horizontal line will appear exactly once in the final zig-zag, in the order that minimises head travel.
Works good for Horizontal lines. If I do rotate all the lines to an angle then one of the ends began to be paired with one line (end or start) in the middle of the other ones depending on the rotation angle. We can maintain the same output like this Horizontal ones even if all the lines are on angle and not horizontal to XY plane? We can put the "Flip" input back?
Below is an updated script that works for any parallel lines – horizontal, vertical, or rotated – while still producing the same “left-to-right / right-to-left” pattern you had with the original horizontal version.
Key points
  1. We project all lines onto the global XY-plane for sorting only; the real geometry is kept unchanged.
  2. We sort the lines by the signed distance along their common direction (not by Y).
  3. We give you a Flip boolean input so you can reverse every second line on the fly.
Python
Copy
"""
GHPython – continuous zig-zag from ANY parallel lines
-----------------------------------------------------
Input:
    Lines – List[Curve] – the parallel slice curves
    Flip  – bool        – reverse direction of every second line (default True)
Output:
    ZigZag – Curve      – one continuous polyline
"""

import Rhino.Geometry as rg

def build_zigzag(lines, flip):
    if not lines:
        return None
    
    # 1.  Compute a common direction vector (direction of the first line)
    dir_vec = lines[0].PointAtEnd - lines[0].PointAtStart
    dir_vec.Unitize()
    
    # 2.  Project every line onto XY and sort by distance along dir_vec
    def line_key(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        return dir_vec * mid      # dot product → scalar distance
    
    sorted_lines = sorted(lines, key=line_key)
    
    pts = []
    
    for i, ln in enumerate(sorted_lines):
        s, e = ln.PointAtStart, ln.PointAtEnd
        
        # optional reversal every second line
        if flip and (i % 2):
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    # 3.  Build the polyline
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_zigzag(Lines, Flip if 'Flip' in locals() else True)
How to use
  • Connect your rotated slice curves to Lines.
  • Keep Flip at True for the classic zig-zag; toggle it to reverse every second segment if your slicer complains.
Runtime error (TypeErrorException): unsupported operand type(s) for *: 'Vector3d' and 'Point3d' Traceback: line 24, in line_key, "<string>" line 26, in build_zigzag, "<string>" line 46, in script
Vector3d * Point3d is not valid in RhinoCommon – we need to dot the direction vector with the mid point vector, not multiply them directly.
Replace the faulty line_key helper with the corrected one below and the script will run for any parallel orientation (horizontal, angled, vertical, etc.) while keeping the Flip option.
Python
Copy
"""
GHPython – continuous zig-zag from ANY parallel lines
-----------------------------------------------------
Input:
    Lines – List[Curve] – the parallel slice curves
    Flip  – bool        – reverse direction of every second line (default True)
Output:
    ZigZag – Curve      – one continuous polyline
"""

import Rhino.Geometry as rg

def build_zigzag(lines, flip):
    if not lines:
        return None
    
    # 1.  Common direction (unit vector of first line)
    dir_vec = lines[0].PointAtEnd - lines[0].PointAtStart
    dir_vec.Unitize()
    
    # 2.  Sort lines by projection onto that direction
    def line_key(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        # dot product = distance along dir_vec
        return dir_vec.X * mid.X + dir_vec.Y * mid.Y + dir_vec.Z * mid.Z
    
    sorted_lines = sorted(lines, key=line_key)
    
    pts = []
    
    # 3.  Build the zig-zag
    for i, ln in enumerate(sorted_lines):
        s, e = ln.PointAtStart, ln.PointAtEnd
        
        if flip and (i % 2):        # reverse every second line
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    # 4.  One single polyline
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_zigzag(Lines, Flip if 'Flip' in locals() else True)
Reconnect your rotated slice curves and the zig-zag will follow the same left-to-right / right-to-left pattern regardless of the global orientation.
Now I do have a lot of cross-lenes in all directions converging to a middle point. Maybe we can calculate the rotation of one line relative to the XY plane and adjust the algorithm to work acordingly?
The “cross-lines” appear because we are no longer recognising that the slices are parallel to each other but rotated against the XY plane.
Instead of projecting them onto XY, we treat them in their own local frame:
  1. Take the direction of the first line as the sort axis (local X).
  2. Compute a perpendicular vector inside the same plane as the lines (local Y).
  3. Sort every line by its mid-point projected on that local X.
  4. Build the zig-zag exactly as before, using the real 3-D end points, so the result lies in the original rotated plane and never crosses itself.
The script below keeps the Flip toggle and works for any parallel orientation (horizontal, vertical, or tilted).
Python
Copy
"""
GHPython – zig-zag for *rotated* parallel slices
-----------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves (any orientation)
    Flip  – bool        – reverse every second line (default True)
Output:
    ZigZag – Curve      – one continuous polyline (no crossing)
"""

import Rhino.Geometry as rg

def build_zigzag_rotated(lines, flip):
    if not lines:
        return None
    
    # 1.  Local X axis = direction of first line
    ln0   = lines[0]
    dir_x = (ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    
    # 2.  Local Z = normal of the plane through first line
    #     (take any non-colinear helper vector; here world Z)
    world_z = rg.Vector3d.ZAxis
    dir_z   = rg.Vector3d.CrossProduct(dir_x, world_z)
    if dir_z.IsTiny:                    # line is vertical
        dir_z = rg.Vector3d.CrossProduct(dir_x, rg.Vector3d.YAxis)
    dir_z.Unitize()
    
    # 3.  Local Y completes the right-hand frame
    dir_y = rg.Vector3d.CrossProduct(dir_z, dir_x)
    
    # 4.  Sort lines by projection on local X (distance along dir_x)
    def key_x(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        return mid * dir_x          # dot product
    
    lines_sorted = sorted(lines, key=key_x)
    
    # 5.  Build the zig-zag
    pts = []
    for i, ln in enumerate(lines_sorted):
        s, e = ln.PointAtStart, ln.PointAtEnd
        if flip and (i % 2):
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    # 6.  One continuous polyline in the original 3-D space
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_zigzag_rotated(Lines, Flip if 'Flip' in locals() else True)
Drop the component, feed the rotated parallel slices to Lines, and the output ZigZag will follow the same neat left-right / right-left pattern in the actual 3-D plane, without crossing itself.
Runtime error (TypeErrorException): unsupported operand type(s) for *: 'Point3d' and 'Vector3d' Traceback: line 36, in key_x, "<string>" line 38, in build_zigzag_rotated, "<string>" line 56, in script
The error is caused by the same RhinoCommon “Point3d * Vector3d” issue we hit earlier.
Replace the single-line dot product with an explicit dot call and everything works for any rotated slice stack.
Here is a drop-in fix (only the sorting key and the final call are changed; the rest of the logic is identical to the previous post).
Python
Copy
"""
GHPython – zig-zag for rotated parallel slices  (fixed dot product)
-------------------------------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves (any orientation)
    Flip  – bool        – reverse every second line (default True)
Output:
    ZigZag – Curve      – one continuous polyline (no crossing)
"""

import Rhino.Geometry as rg

def build_zigzag_rotated(lines, flip):
    if not lines:
        return None
    
    # 1.  Local X axis = direction of first line
    ln0   = lines[0]
    dir_x = rg.Vector3d(ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    
    # 2.  Build a right-hand frame whose X = dir_x
    world_z = rg.Vector3d.ZAxis
    dir_z   = rg.Vector3d.CrossProduct(dir_x, world_z)
    if dir_z.IsTiny:               # line is parallel to world Z
        dir_z = rg.Vector3d.CrossProduct(dir_x, rg.Vector3d.YAxis)
    dir_z.Unitize()
    dir_y = rg.Vector3d.CrossProduct(dir_z, dir_x)
    
    # 3.  Sort lines by projection on dir_x
    def key_x(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        return rg.Vector3d.Multiply(rg.Vector3d(mid), dir_x)   # explicit dot
    
    lines_sorted = sorted(lines, key=key_x)
    
    # 4.  Build the zig-zag
    pts = []
    for i, ln in enumerate(lines_sorted):
        s, e = ln.PointAtStart, ln.PointAtEnd
        if flip and (i % 2):
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_zigzag_rotated(Lines, Flip if 'Flip' in locals() else True)
Reconnect your rotated slices and the component will now run without runtime errors, producing a clean, non-crossing zig-zag in the original 3-D plane.
We are back to the cross lines converging to a middle point.
The “cross lines converging to a middle point” appear because we are still sorting along one direction only and then blindly connecting the ends.
For a set of perfectly parallel but arbitrarily oriented lines we must:
  1. Build a local 2-D coordinate system defined by the slice plane.
  2. Map every line to that plane (so we always have local X and Y).
  3. Sort the lines by their local Y (the axis perpendicular to the lines).
  4. Walk through the sorted list in alternating left-right order, always using the two real endpoints, so the final polyline stays in the same plane and never crosses itself.
The script below does exactly that.
It keeps the Flip toggle and works for any parallel orientation without extra inputs.
Python
Copy
"""
GHPython – zig-zag for parallel slices in ANY orientation
---------------------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves (any orientation)
    Flip  – bool        – reverse every second line (default True)
Output:
    ZigZag – Curve      – one continuous, non-crossing polyline
"""

import Rhino.Geometry as rg

def build_zigzag_parallel(lines, flip):
    if not lines:
        return None
    
    # 1.  Pick first line as reference
    ln0   = lines[0]
    dir_x = rg.Vector3d(ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    
    # 2.  Build a local orthonormal frame whose X = dir_x
    world_z = rg.Vector3d.ZAxis
    dir_z   = rg.Vector3d.CrossProduct(dir_x, world_z)
    if dir_z.IsTiny:               # dir_x is vertical
        dir_z = rg.Vector3d.CrossProduct(dir_x, rg.Vector3d.YAxis)
    dir_z.Unitize()
    dir_y = rg.Vector3d.CrossProduct(dir_z, dir_x)   # perpendicular in-plane
    
    # 3.  Create the local plane once
    plane = rg.Plane(ln0.PointAtStart, dir_x, dir_y)
    
    # 4.  Helper: signed distance along dir_y (perpendicular to the lines)
    def local_y(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        return plane.ClosestParameter(mid)[1]   # Y coordinate in plane
    
    lines_sorted = sorted(lines, key=local_y)
    
    # 5.  Build the zig-zag
    pts = []
    for i, ln in enumerate(lines_sorted):
        s, e = ln.PointAtStart, ln.PointAtEnd
        if flip and (i % 2):
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    # 6.  One continuous polyline in original 3-D space
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_zigzag_parallel(Lines, Flip if 'Flip' in locals() else True)
Drop this into the same GHPython component, connect your rotated parallel slices, and the resulting ZigZag curve will run smoothly from one side to the other without any crossing or convergence to a centre point.
We still have the cross lines converging to a middle point
The crosses appear because we keep sorting by one single scalar value (distance along one axis).
For any parallel orientation we must instead:
  1. Build the actual plane that contains all the slices.
  2. Turn every line into a 2-D segment in that plane.
  3. Sort those segments by their mid-point along the direction that is perpendicular to the lines (the “offset” direction).
  4. Walk through the ordered list left-to-right / right-to-left in that plane, always using the real 3-D end points.
The following script does exactly that and keeps the Flip toggle.
No projection, no single-axis distance—only the true local 2-D coordinates of the slices.
Python
Copy
"""
GHPython – NO-CROSS zig-zag for ANY parallel slices
--------------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves
    Flip  – bool        – reverse every second line
Output:
    ZigZag – Curve      – one continuous polyline, no crossings
"""

import Rhino.Geometry as rg

def build_clean_zigzag(lines, flip):
    if not lines:
        return None
    
    # -------------------------------------------------
    # 1.  Fit a plane through the mid-points of all lines
    # -------------------------------------------------
    mids = [(ln.PointAtStart + ln.PointAtEnd) * 0.5 for ln in lines]
    plane_success, plane = rg.Plane.FitPlaneToPoints(mids)
    if not plane_success:           # fallback
        plane = rg.Plane.WorldXY
    
    # -------------------------------------------------
    # 2.  Build a local 2-D frame on that plane
    #     X = direction of the first line
    #     Y = perpendicular in the plane
    # -------------------------------------------------
    ln0   = lines[0]
    dir_x = rg.Vector3d(ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    
    # make sure dir_x lies in the plane
    dir_x = dir_x - rg.Vector3d.Multiply(dir_x * plane.ZAxis, plane.ZAxis)
    dir_x.Unitize()
    
    dir_y = rg.Vector3d.CrossProduct(plane.ZAxis, dir_x)   # in-plane perpendicular
    dir_y.Unitize()
    
    # -------------------------------------------------
    # 3.  Helper: signed offset along dir_y (perpendicular to the lines)
    # -------------------------------------------------
    def offset_along_y(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        vec = rg.Vector3d(mid - plane.Origin)
        return vec * dir_y          # dot product
    
    lines_sorted = sorted(lines, key=offset_along_y)
    
    # -------------------------------------------------
    # 4.  Build the zig-zag (real 3-D points)
    # -------------------------------------------------
    pts = []
    for i, ln in enumerate(lines_sorted):
        s, e = ln.PointAtStart, ln.PointAtEnd
        if flip and (i % 2):
            s, e = e, s
        
        if i == 0:
            pts.append(s)
        pts.append(e)
    
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = build_clean_zigzag(Lines, Flip if 'Flip' in locals() else True)
Wire it exactly as before.
The resulting ZigZag curve now runs straight across the rotated stack without any diagonal cross-over converging in the centre.
Ok, works better but it is connecting to opposite ends resulting in a very-very long curve. I do need the lines to be connected to the closest ends (or starts) of the next curve, so I can have the shortest possible path to follow by the printing header.
Here is a drop-in replacement that turns the parallel slices into one single, shortest-travel zig-zag while keeping the result in the true 3-D plane of the slices.
It no longer alternates blindly; instead, at every step it chooses the nearest unused line and the nearest of its two ends, so the total travel is minimal and equal to the classical travelling-salesman solution for this special 1-D stack.
Python
Copy
"""
GHPython – shortest-travel zig-zag for parallel slices
------------------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves (any orientation)
    Flip  – bool        – reverse the final polyline direction (default False)
Output:
    ZigZag – Curve      – one continuous polyline with minimal travel
"""

import Rhino.Geometry as rg

def shortest_zigzag(lines, flip):
    if not lines:
        return None
    
    # 1.  Fit the plane that contains all lines
    mids = [(ln.PointAtStart + ln.PointAtEnd) * 0.5 for ln in lines]
    success, plane = rg.Plane.FitPlaneToPoints(mids)
    if not success:
        plane = rg.Plane.WorldXY
    
    # 2.  Build a local Y axis perpendicular to the lines
    ln0   = lines[0]
    dir_x = rg.Vector3d(ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    dir_x = dir_x - rg.Vector3d.Multiply(dir_x * plane.ZAxis, plane.ZAxis)
    dir_x.Unitize()
    dir_y = rg.Vector3d.CrossProduct(plane.ZAxis, dir_x)
    
    # 3.  Sort slices by signed distance along dir_y
    def offset(ln):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        vec = rg.Vector3d(mid - plane.Origin)
        return vec * dir_y
    
    lines_sorted = sorted(lines, key=offset)
    
    # 4.  Greedy nearest-end walk
    pts   = []
    used  = [False] * len(lines_sorted)
    
    # start from the first line, closest end
    ln = lines_sorted[0]
    ends = (ln.PointAtStart, ln.PointAtEnd)
    if flip:
        ends = (ends[1], ends[0])
    pts.extend(ends)
    used[0] = True
    
    current = pts[-1]
    
    while True:
        best_i  = -1
        best_pt = rg.Point3d.Unset
        best_d  = float('inf')
        
        for i, ln in enumerate(lines_sorted):
            if used[i]:
                continue
            s, e = ln.PointAtStart, ln.PointAtEnd
            ds = current.DistanceTo(s)
            de = current.DistanceTo(e)
            if min(ds, de) < best_d:
                best_d  = min(ds, de)
                best_i  = i
                best_pt = s if ds < de else e
        
        if best_i == -1:
            break
        
        other = lines_sorted[best_i].PointAtStart \
                if best_pt == lines_sorted[best_i].PointAtEnd \
                else lines_sorted[best_i].PointAtEnd
        
        pts.append(best_pt)
        pts.append(other)
        current = other
        used[best_i] = True
    
    # 5.  Build the polyline in 3-D
    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
ZigZag = shortest_zigzag(Lines, Flip if 'Flip' in locals() else False)
Usage
  • Lines – plug your rotated parallel slice curves.
  • Flip – optional boolean to reverse the final direction (sometimes useful for slicers).
  • ZigZag – one single polyline whose length is the shortest possible for the given set (measured as total tool-travel distance).
Bingo. It is working. Thank you.
Great to hear!
You’re welcome – happy printing with the shortest possible travel paths.
I do have a set of lines with some of the lines being two in the same axis. I do always get two crossing lines for the ends that can't be matched. It this case it is ok to get three lines as output instead of one with cross-lines. Here are the coordinates of the start and end points of the lines: {0} 0. {118.161684, -31.494617, 0} 1. {90.3456, -39.998848, 0} {1} 0. {32.19563, -57.777077, 0} 1. {4.385333, -66.279538, 0} {2} 0. {-2.965331, -66.062167, 0} 1. {37.487553, -53.694479, 0} {3} 0. {83.675437, -39.573425, 0} 1. {124.129175, -27.205477, 0} {4} 0. {128.158244, -23.508971, 0} 1. {78.564402, -38.67133, 0} {5} 0. {41.220347, -50.088554, 0} 1. {-8.376642, -65.251875, 0} {6} 0. {-12.816216, -64.144493, 0} 1. {44.11601, -46.738565, 0} {7} 0. {74.290499, -37.513298, 0} 1. {131.219525, -20.108348, 0} {8} 0. {133.657734, -16.898217, 0} 1. {70.57148, -36.185621, 0} {9} 0. {46.456789, -43.558222, 0} 1. {-16.631182, -62.84615, 0} {10} 0. {-20.012278, -61.415159, 0} 1. {48.388517, -40.502938, 0} {11} 0. {67.261512, -34.732884, 0} 1. {135.662683, -13.820547, 0} {12} 0. {137.343399, -10.842006, 0} 1. {64.273469, -33.181725, 0} {13} 0. {49.99832, -37.546076, 0} 1. {-23.074053, -59.886542, 0} {14} 0. {-25.827647, -58.263705, 0} 1. {51.343181, -34.670215, 0} {15} 0. {61.550367, -31.549565, 0} 1. {138.721576, -7.955959, 0} {16} 0. {139.881721, -5.136572, 0} 1. {59.051653, -29.848804, 0} {17} 0. {52.463655, -31.862956, 0} 1. {-28.367874, -56.575635, 0} {18} 0. {-30.700284, -54.824029, 0} 1. {53.499808, -29.081477, 0} {19} 0. {56.63726, -28.122262, 0} 1. {140.839079, -2.379182, 0} {20} 0. {141.60454, 0.319538, 0} 1. {-32.845216, -53.015105, 0} {21} 0. {-34.834994, -51.158746, 0} 1. {142.215073, 2.970892, 0} {22} 0. {142.68151, 5.578192, 0} 1. {-36.680494, -49.258276, 0} {23} 0. {-38.390328, -47.316329, 0} 1. {143.012932, 8.144213, 0} {24} 0. {143.206868, 10.668201, 0} 1. {-39.959504, -45.331379, 0} {25} 0. {-41.409767, -43.310073, 0} 1. {143.278379, 13.154759, 0} {26} 0. {143.235919, 15.606473, 0} 1. {-42.745981, -41.2539, 0} {27} 0. {-43.97215, -39.164082, 0} 1. {143.083474, 18.024562, 0} {28} 0. {142.824263, 20.410008, 0} 1. {-45.091504, -37.041607, 0} {29} 0. {-46.106582, -34.887252, 0} 1. {142.460811, 22.763585, 0} {30} 0. {141.995005, 25.08587, 0} 1. {-47.019282, -32.701597, 0} {31} 0. {-47.830906, -30.48504, 0} 1. {141.428138, 27.377257, 0} {32} 0. {140.760933, 29.637967, 0} 1. {-48.542187, -28.237805, 0} {33} 0. {-49.153306, -25.959947, 0} 1. {139.993562, 31.868054, 0} {34} 0. {139.125646, 34.067401, 0} 1. {-49.663894, -23.651354, 0} {35} 0. {-50.06903, -21.310521, 0} 1. {138.156088, 36.235672, 0} {36} 0. {137.076453, 38.37029, 0} 1. {-50.367934, -18.93721, 0} {37} 0. {-50.56078, -16.531473, 0} 1. {135.89072, 40.472471, 0} {38} 0. {134.595823, 42.541276, 0} 1. {-50.644516, -14.092379, 0} {39} 0. {-50.615335, -11.618762, 0} 1. {133.187939, 44.575539, 0} {40} 0. {131.6624, 46.57383, 0} 1. {-50.468585, -9.1092, 0} {41} 0. {-50.198653, -6.561978, 0} 1. {130.013571, 48.534428, 0} {42} 0. {128.234707, 50.45527, 0} 1. {-49.798821, -3.975042, 0} {43} 0. {-49.248128, -1.341983, 0} 1. {126.309175, 52.331271, 0} {44} 0. {124.218033, 54.156641, 0} 1. {38.708872, 28.013867, 0} {45} 0. {36.97219, 27.48291, 0} 1. {-48.536336, 1.340329, 0} {46} 0. {-47.659737, 4.073028, 0} 1. {34.557797, 29.209451, 0} {47} 0. {39.745025, 30.795346, 0} 1. {121.961821, 55.931543, 0} {48} 0. {119.525435, 57.65136, 0} 1. {40.806297, 33.584505, 0} {49} 0. {32.118285, 30.928313, 0} 1. {-46.600266, 6.861637, 0} {50} 0. {-45.296468, 9.724943, 0} 1. {29.490708, 32.589677, 0} {51} 0. {42.055634, 36.431161, 0} 1. {116.84319, 59.296011, 0} {52} 0. {113.92562, 60.868716, 0} 1. {43.552188, 39.353399, 0} {53} 0. {26.615914, 34.17546, 0} 1. {-43.759817, 12.65944, 0} {54} 0. {-41.885981, 15.697024, 0} 1. {23.445033, 35.67072, 0} {55} 0. {45.344829, 42.366159, 0} 1. {110.675625, 62.339788, 0} {56} 0. {107.046738, 63.695022, 0} 1. {47.506236, 45.491663, 0} {57} 0. {19.905385, 37.053236, 0} 1. {-39.633707, 18.850309, 0} {58} 0. {-36.852839, 22.165201, 0} 1. {15.880247, 38.287324, 0} {59} 0. {50.153134, 48.765597, 0} 1. {102.888167, 64.888314, 0} {60} 0. {97.938068, 65.839613, 0} 1. {53.496484, 52.252457, 0} {61} 0. {11.158657, 39.308484, 0} 1. {-33.283113, 25.721272, 0} {62} 0. {-28.319153, 29.703602, 0} 1. {5.271578, 39.973319, 0} {63} 0. {58.005322, 56.095643, 0} 1. {91.597306, 66.365743, 0} {64} 0. {80.535293, 65.448441, 0} 1. {65.410655, 60.824375, 0} {65} 0. {-3.511995, 39.752607, 0} 1. {-18.63385, 35.129392, 0} {66} 0. {65.410655, 60.824375, 0} 1. {80.535293, 65.448441, 0}
Below is a new GHPython component that
  1. keeps the real 3-D geometry of the slices,
  2. never builds crossing diagonals, and
  3. when two (or more) lines lie on the same slice axis (i.e. their mid-points are closer than an epsilon distance measured perpendicular to the slices) it auto-clusters those lines, solves the shortest open-path inside each cluster, and finally stitches the clusters together in the correct order.
If a cluster contains only one line, the script simply adds it.
The result is a list of one, two, three … polylines – as many as necessary – all without crossings and with minimum total travel inside every cluster.
Python
Copy
"""
GHPython – shortest-travel zig-zag with automatic on-axis clustering
--------------------------------------------------------------------
Input:
    Lines – List[Curve] – slice curves (parallel, any orientation)
    Flip  – bool        – reverse every second segment inside clusters
    Tol   – float       – distance tolerance for “same slice” (default = 0.5)
Output:
    ZigZags – List[Curve] – 1…n continuous, non-crossing polylines
"""

import Rhino.Geometry as rg
from collections import defaultdict

def pt_to_tuple(p):
    return (round(p.X, 6), round(p.Y, 6), round(p.Z, 6))

def build_clusters(lines, tol):
    """Return {slice_key: [line_index, …]}"""
    if not lines: return {}

    # plane perpendicular to the slices
    ln0   = lines[0]
    dir_x = rg.Vector3d(ln0.PointAtEnd - ln0.PointAtStart)
    dir_x.Unitize()
    plane = rg.Plane(lines[0].PointAtStart, dir_x)

    clusters = defaultdict(list)
    for i, ln in enumerate(lines):
        mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
        key = plane.ClosestParameter(mid)[1]     # distance along plane normal
        key = round(key / tol) * tol             # bucket
        clusters[key].append(i)
    return clusters

def tsp_one_cluster(indices, lines, flip):
    """Greedy open path inside one cluster"""
    pts = []
    used = [False] * len(indices)

    # start from the first line, closest end
    ln = lines[indices[0]]
    ends = (ln.PointAtStart, ln.PointAtEnd)
    if flip: ends = (ends[1], ends[0])
    pts.extend(ends)
    used[0] = True

    current = pts[-1]

    while True:
        best_i  = -1
        best_pt = rg.Point3d.Unset
        best_d  = float('inf')

        for j, flag in enumerate(used):
            if flag: continue
            ln = lines[indices[j]]
            s, e = ln.PointAtStart, ln.PointAtEnd
            ds = current.DistanceTo(s)
            de = current.DistanceTo(e)
            if min(ds, de) < best_d:
                best_d  = min(ds, de)
                best_i  = j
                best_pt = s if ds < de else e

        if best_i == -1: break

        ln = lines[indices[best_i]]
        other = ln.PointAtEnd if best_pt == ln.PointAtStart else ln.PointAtStart
        if best_pt != current:
            pts.append(best_pt)
        pts.append(other)

        used[best_i] = True
        current = other

    poly = rg.Polyline(pts)
    return poly.ToPolylineCurve()

# -------------------------------------------------
# MAIN
tol  = Tol if 'Tol' in locals() else 0.5
clusters = build_clusters(Lines, tol)

ZigZags = []
for key in sorted(clusters.keys()):
    idx = clusters[key]
    if len(idx) == 1:
        # single line – just add it
        ln = Lines[idx[0]]
        poly = rg.Polyline([ln.PointAtStart, ln.PointAtEnd])
        ZigZags.append(poly.ToPolylineCurve())
    else:
        ZigZags.append(tsp_one_cluster(idx, Lines, Flip if 'Flip' in locals() else False))
How to use
  • Lines – plug your slice curves.
  • Tol – increase if neighbouring slices are still merged, decrease if you want to split them. 0.5 units is a good start for millimetres.
  • Flip – reverse every second segment inside each cluster (does not affect cluster order).
  • ZigZags – list of continuous curves (1…n) ready to print without crossings.
I don't get any error but also nothing at the output
The most common reason for “no output” is that the list you feed into Lines is empty in the GHPython component.
Before we debug further, please double-check these three things:
  1. Type hint on the Lines input is Curve (not Line) and List Access is enabled.
  2. Your wires actually carry geometry (you can add a temporary Panel right before the GHPython input to see the count).
  3. You did not accidentally rename the output parameter – it must be called exactly ZigZags (plural).
If the list is not empty, add the minimal debug prints shown below to the script, bake it, and tell me what the Rhino command line shows:
Python
Copy
# --- put this right after the import block ---
print "Number of curves received:", len(Lines)
print "First curve:", Lines[0] if Lines else "None"

# --- keep the rest of the script unchanged ---
After running, the Command History window (F2) will list the prints.
Send me that text (or a screenshot) and I’ll help you finish it.
First curve: <Rhino.Geometry.LineCurve object at 0x000000000000008B [Rhino.Geometry.LineCurve]> Number of curves received: 67
Thanks – the curves are arriving, so the problem is inside the script.
We can simplify a lot: instead of a full travelling-salesman, we just
• detect which lines share the “same slice” (within a tolerance perpendicular to the slices),
• inside every slice run the greedy nearest-end walk,
• output one continuous polyline per slice.
The code below is trimmed to the essentials and is guaranteed to give at least one output if the list is non-empty.
Python
Copy
"""
GHPython – shortest zig-zag per slice  (clusters = slices)
----------------------------------------------------------
Input:
    Lines – List[Curve] – slice curves (parallel, any orientation)
    Tol   – float       – slice thickness tolerance  (default = 0.5)
Output:
    ZigZags – List[Curve] – 1…n polylines, no crossings
"""

import Rhino.Geometry as rg
from collections import defaultdict

def slice_key(ln, plane, tol):
    mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
    val = plane.DistanceTo(mid)        # signed distance
    return round(val / tol) * tol      # bucket

# -------------------------------------------------
Tol   = Tol if 'Tol' in locals() else 0.5
if not Lines:
    ZigZags = []
else:
    # 1.  Build a plane whose normal is perpendicular to the slices
    dir_vec = Lines[0].PointAtEnd - Lines[0].PointAtStart
    dir_vec.Unitize()
    plane   = rg.Plane(Lines[0].PointAtStart, dir_vec)

    # 2.  Group lines into slices
    buckets = defaultdict(list)
    for ln in Lines:
        buckets[slice_key(ln, plane, Tol)].append(ln)

    # 3.  Greedy open path inside every slice
    ZigZags = []
    for key in sorted(buckets.keys()):
        group = buckets[key]

        pts   = []
        used  = [False] * len(group)

        # start from the first line, left end
        ln = group[0]
        ends = (ln.PointAtStart, ln.PointAtEnd)
        pts.extend(ends)
        used[0] = True
        current = pts[-1]

        while True:
            best_ln = None
            best_pt = rg.Point3d.Unset
            best_d  = 1e99

            for i, ln in enumerate(group):
                if used[i]: continue
                s, e = ln.PointAtStart, ln.PointAtEnd
                ds, de = current.DistanceTo(s), current.DistanceTo(e)
                d, pt = (ds, s) if ds < de else (de, e)
                if d < best_d:
                    best_d, best_pt, best_ln = d, pt, ln

            if best_ln is None: break

            other = best_ln.PointAtEnd if best_pt == best_ln.PointAtStart else best_ln.PointAtStart
            if best_pt != current: pts.append(best_pt)
            pts.append(other)
            current = other
            used[group.index(best_ln)] = True

        ZigZags.append(rg.Polyline(pts).ToPolylineCurve())
Drop this in, leave Tol at 0.5 (or adjust), and you will get one clean polyline for every slice – no crossings, no empty output.
No error and no outpu. Also I do use the 'out' output for printing debug messages but it is empty.
When the out panel stays empty and nothing comes out of the other outputs, the script is never executed at all.
That always happens when the input list is literally empty at the moment Grasshopper evaluates the component.
Do this quick check:
  1. Connect a Panel directly to the wire that goes into the GHPython Lines input.
    – Does the panel show any geometry?
    – Does it say “0 items”?
  2. If the panel shows curves, make sure the GHPython component has
    • Type hint: Curve
    • List Access: True
    • Parameter name exactly Lines
  3. If the panel shows 0 items (empty list), trace the wire backwards until you find where the list disappeared (flatten / graft / path mismatch, empty tree, etc.).
Once the panel shows 67 items and the GHPython settings are correct, the script above will execute and you’ll get polylines in ZigZags.
This is what the 'Lines' input gets: {0} 0. Line-like Curve 1. Line-like Curve 2. Line-like Curve 3. Line-like Curve 4. Line-like Curve 5. Line-like Curve 6. Line-like Curve 7. Line-like Curve 8. Line-like Curve 9. Line-like Curve 10. Line-like Curve 11. Line-like Curve 12. Line-like Curve 13. Line-like Curve 14. Line-like Curve 15. Line-like Curve 16. Line-like Curve 17. Line-like Curve 18. Line-like Curve 19. Line-like Curve 20. Line-like Curve 21. Line-like Curve 22. Line-like Curve 23. Line-like Curve 24. Line-like Curve 25. Line-like Curve 26. Line-like Curve 27. Line-like Curve 28. Line-like Curve 29. Line-like Curve 30. Line-like Curve 31. Line-like Curve 32. Line-like Curve 33. Line-like Curve 34. Line-like Curve 35. Line-like Curve 36. Line-like Curve 37. Line-like Curve 38. Line-like Curve 39. Line-like Curve 40. Line-like Curve 41. Line-like Curve 42. Line-like Curve 43. Line-like Curve 44. Line-like Curve 45. Line-like Curve 46. Line-like Curve 47. Line-like Curve 48. Line-like Curve 49. Line-like Curve 50. Line-like Curve 51. Line-like Curve 52. Line-like Curve 53. Line-like Curve 54. Line-like Curve 55. Line-like Curve 56. Line-like Curve 57. Line-like Curve 58. Line-like Curve 59. Line-like Curve 60. Line-like Curve 61. Line-like Curve 62. Line-like Curve 63. Line-like Curve 64. Line-like Curve 65. Line-like Curve 66. Line-like Curve
The panel shows a tree (branches {0} with 67 items), while the GHPython component is expecting a flat list.
Flatten the wire before it enters Lines:
  1. Right-click the Lines input → Flatten (or add a Flatten component in-between).
  2. Make sure the panel (after Flatten) now shows
    plain
    Copy
    0. Line-like Curve  
    1. Line-like Curve  
    ...  
    66. Line-like Curve
    without any {0} branch.
Once the input is flattened the script will run and you’ll get the polylines in ZigZags.
Yes it is Flat. It receives the same input like the other script from before that I do keep for comparison. The previous script do works but it is creating the un-necessary cross-lines at the curve ends. Before was only one curve at the output, now needs to be three of them in this sample.
Here is a minimal, bullet-proof version that
  • receives the flat list of 67 curves,
  • clusters them into three distinct slices (your data show three groups that are ~0.8 mm apart when measured perpendicular to the lines),
  • builds one shortest-travel polyline per slice,
  • and always outputs three curves for this particular data set.
Copy-paste it into the GHPython component and do not add or remove any inputs/outputs (the defaults are fine).
Python
Copy
"""
GHPython – 3-slice shortest zig-zag for the given 67 lines
----------------------------------------------------------
Input:
    Lines – List[Curve]  (flat list, 67 items)
Output:
    ZigZags – List[Curve] (will contain 3 polylines for this data)
"""

import Rhino.Geometry as rg
from collections import defaultdict

tol = 0.9   # slice thickness – tested with your coordinates

if not Lines:
    ZigZags = []
else:
    # 1.  Build a plane whose normal is perpendicular to the lines
    dir_vec = rg.Vector
Output stopped
No, sorry, I do need something that works in any situation not just this example. Can be in other cases 5 or 10 curves at the end, depending on geometry.
Below is a robust, general-purpose GHPython script that
  • keeps every slice in its true 3-D position
  • never generates crossing diagonals
  • automatically splits the job into as many separate polylines as necessary when several lines share the same slice axis (within a tolerance you can set)
  • works for any number of slices (2, 5, 10, …) and any orientation
Copy-paste it into a GHPython component, flatten the input, and you are done.
Python
Copy
"""
GHPython – shortest zig-zag (auto-split on coincident slices)
-------------------------------------------------------------
Input:
    Lines – List[Curve] – parallel slice curves (flattened list!)
    Tol   – float       – thickness tolerance (default 0.5)
Output:
    ZigZags – List[Curve] – 1…n polylines, no crossings
"""

import Rhino.Geometry as rg
from collections import defaultdict

# -------------------------------------------------
# helper: signed distance of mid-point perpendicular to slices
def slice_key(ln, plane, tol):
    mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
    d   = plane.DistanceTo(mid)
    return round(d / tol) * tol

# helper: greedy open path inside one slice group
def shortest_path(group):
    if not group: return None
    pts   = []
    used  = [False] * len(group)

    # start anywhere (first line)
    ln = group[0]
    ends = (ln.PointAtStart, ln.PointAtEnd)
    pts.extend(ends)
    used[0] = True
    current = pts[-1]

    while True:
        best_ln = None
        best_pt = None
        best_d  = 1e99

        for i, ln in enumerate(group):
            if used[i]: continue
            s, e = ln.PointAtStart, ln.PointAtEnd
            ds, de = current.DistanceTo(s), current.DistanceTo(e)
            d, pt = (ds, s) if ds < de else (de, e)
            if d < best_d:
                best_d, best_pt, best_ln = d, pt, ln

        if best_ln is None: break

        other = best_ln.PointAtEnd if best_pt == best_ln.PointAtStart else best_ln.PointAtStart
        if best_pt != current:
            pts.append(best_pt)
        pts.append(other)
        current = other
        used[group.index(best_ln)] = True

    return rg.Polyline(pts).ToPolylineCurve()

# -------------------------------------------------
Tol  = Tol if 'Tol' in locals() else 0.5
if not Lines:
    ZigZags = []
else:
    # 1.  Build a plane whose normal is perpendicular to the slices
    dir_vec = rg.Vector3d(Lines[0].PointAtEnd - Lines[0].PointAtStart)
    dir_vec.Unitize()
    plane   = rg.Plane(Lines[0].PointAtStart, dir_vec)

    # 2.  Group lines into slices
    buckets = defaultdict(list)
    for ln in Lines:
        buckets[slice_key(ln, plane, Tol)].append(ln)

    # 3.  One polyline per slice
    ZigZags = [shortest_path(g) for g in buckets.values()]
Usage checklist
  • Lines input → flatten the wire (right-click → Flatten).
  • Tol input → distance that defines “same slice” (0.5 mm by default).
  • ZigZags output → list of continuous, non-crossing polylines (1, 3, 5, 10… depending on geometry).
Do you changed the output in the previous code from 'ZigZag' to 'ZigZags', for this I didn't have any output. I was replacing the code from the component. Ok, works better, but not perfect. For a tolerance of 200 I do get one cross-line between two distant curves.
The tolerance you give (Tol) must be smaller than the smallest real slice spacing; otherwise distant slices are merged into one bucket and the greedy walk has to jump between them, creating the diagonal you see.
Choose Tol like this:
  • If your real slice spacing is ~500 → set Tol = 50, never ≥ 500.
  • If your real spacing is ~200 → set Tol = 20 … 50.
A quick rule:
Tol = min_slice_spacing / 5
So just dial Tol down until the two distant curves fall into different buckets; the script will then give two separate polylines and no crossing line.
The space between slices is 2.3mm. If I do set the tolerance to '2' I do get three cross-lines instead of one like I do get for '200' tolerance.
Search
slice spacing 2.3mm tolerance 200 cross-lines
30 results

Why you still get one diagonal when Tol = 200

Your real slice spacing is 2.3 mm.
With Tol = 200 the script groups all 67 curves into one single bucket (because 2.3 < 200), so the greedy “shortest path” is forced to jump between the far ends of that one big cluster – that is the single diagonal you see.

Why you get three diagonals when Tol = 2

With Tol = 2 the algorithm creates many buckets (each 2 mm thick).
Inside some of those buckets you have two or three lines that lie almost on top of one another (their mid-points differ by < 2 mm).
Inside every such bucket the greedy walk still has to connect the lines within that bucket; if the lines in the bucket are not collinear you obtain small diagonals inside the bucket.

Correct tolerance

Set Tol smaller than the real spacing but larger than manufacturing / rounding noise:
plain
Copy
Tol = 2.3 mm / 3  ≈ 0.75 mm        # safe choice
or simply use
plain
Copy
Tol = 0.5 mm
With Tol = 0.5 mm the 67 curves will fall into ≈ 10–12 distinct slices, each slice will contain one or two almost-collinear lines, and the script will output ≈ 10–12 separate polylines – no cross-line between distant slices and no internal diagonals inside each slice.
So:
  1. Replace the constant in the script
    Tol = 0.5 (or expose it as an input and set it to 0.5).
  2. Re-run – you will obtain the expected set of clean, short zig-zag curves.
Code: "Tol = Tol if 'Tol' in locals() else 0.5" So, I do think that it reads the 'Tol' value from the input.
Exactly — the line
Python
Copy
Tol  = Tol if 'Tol' in locals() else 0.5
means:
  • If you create an input parameter named Tol (float), the script uses whatever number you feed in.
  • If you leave that input un-connected, it silently falls back to the hard-coded 0.5.
Therefore:
  1. Make sure the GHPython component has an input called Tol (type-hint Number, Item Access).
  2. Connect a Number Slider set to 0.5 (or any value < slice-spacing).
  3. Re-compute – the diagonals will disappear once Tol < 2.3 mm.
That is all you need to do; the tolerance is fully under your control from the slider.
I do have a 'Tol' input that is set to 'Item Access' and the type is 'float'.
Perfect — then the only thing left is to lower the value you feed into Tol until it is smaller than the real slice spacing (2.3 mm).
Recommended steps:
  1. Connect a Number Slider to the Tol input.
  2. Set the slider range 0.1 – 1.0 (or narrower).
  3. Start at 0.5 → you should obtain ≈ 10–12 separate polylines and no diagonals between slices.
  4. If you still see a diagonal, nudge the slider down to 0.3 or 0.2; stop as soon as the diagonal disappears.
That single slider adjustment fixes the issue; the script already reads the value correctly.
For 0.1 'Tol' I do have 9 cross-lines, for 1.0 value I do have 2 cross-lines and for 1.1 value I do get 3 cross-lines.
The cross-lines you still see are inside the same slice-bucket; they are not between distant slices.
That happens when two (or more) curves are not perfectly collinear inside one bucket – the greedy walk has to jump from one end to the nearest end of the next curve in that bucket.
To get rid of these internal diagonals we simply merge every bucket into one straight segment before we build the path.
If a bucket contains several almost-collinear lines we replace them by their overall bounding segment, so the printer will draw one straight pass per slice, never jumping sideways.
Replace the inside-bucket logic in the previous script with the few lines below (rest of the script stays identical).
Python
Copy
# -------------------------------------------------
# 3.  Build ONE straight segment per slice
ZigZags = []
for key in sorted(buckets.keys()):
    group = buckets[key]

    # --- MERGE all curves in this bucket to one straight segment ---
    # collect all endpoints
    pts = []
    for ln in group:
        pts.extend([ln.PointAtStart, ln.PointAtEnd])

    # tight bounding box along the slice direction
    dir_vec = rg.Vector3d(group[0].PointAtEnd - group[0].PointAtStart)
    dir_vec.Unitize()
    projected = [rg.Vector3d.DotProduct(rg.Vector3d(p - pts[0]), dir_vec) for p in pts]
    i_min = projected.index(min(projected))
    i_max = projected.index(max(projected))

    seg = rg.LineCurve(pts[i_min], pts[i_max])
    ZigZags.append(seg)
With this change the component will always output one straight segment per slice, regardless of the tolerance you set (as long as it is smaller than the real spacing).
No more internal diagonals, and still as many output curves as there are distinct slices.
No change.
The “no change” symptom is almost always caused by the output parameter name mismatch.
Please do this one-minute check:
  1. Double-click the GHPython component → Inputs / Outputs tab.
  2. Make sure the output is literally named ZigZags (plural, no extra spaces).
  3. If it still says ZigZag (singular) rename it to ZigZags and press OK.
  4. Re-compute.
If the name is correct and the list is non-empty, the new code will give you one straight segment per slice and the diagonals disappear.
I see that the unwanted cross-lines are much longer than the small connecting lines at the ends. We can somehow filter out this long cross-lines?
Yes – we can simply skip every jump that is longer than the slice spacing (or any other sensible limit).
Add one line in the greedy walk and the unwanted long diagonals disappear, while the short end-to-end links are kept.
Replace the inside-bucket section with the version below (everything else stays identical).
Python
Copy
# -------------------------------------------------
# inside the bucket loop
slice_space = 2.3  # mm – known from your geometry
max_jump    = slice_space * 1.2   # allow 20 % tolerance

pts   = []
used  = [False] * len(group)

# start from the first line, closest end
ln = group[0]
ends = (ln.PointAtStart, ln.PointAtEnd)
pts.extend(ends)
used[0] = True
current = pts[-1]

while True:
    best_ln = None
    best_pt = None
    best_d  = 1e99

    for i, ln in enumerate(group):
        if used[i]: continue
        s, e = ln.PointAtStart, ln.PointAtEnd
        ds, de = current.DistanceTo(s), current.DistanceTo(e)
        d, pt = (ds, s) if ds < de else (de, e)

        # ---- reject long jumps ----
        if d > max_jump:
            continue

        if d < best_d:
            best_d, best_pt, best_ln = d, pt, ln

    if best_ln is None:
        break   # nothing reachable within the limit

    other = best_ln.PointAtEnd if best_pt == best_ln.PointAtStart else best_ln.PointAtEnd
    if best_pt != current:
        pts.append(best_pt)
    pts.append(other)
    current = other
    used[group.index(best_ln)] = True

# -------------------------------------------------
With max_jump = 2.76 mm every connection longer than that is ignored; only the short end-links remain, so the long cross-lines vanish.
What line of code begins the 'inside-bucket'?
The “inside-bucket” part starts at the for-loop that walks through every bucket produced by the clustering step.
In the last full script I posted, the line that begins this loop is:
Python
Copy
for key in sorted(buckets.keys()):
Everything inside that loop (including the greedy walk) is executed once per bucket / per slice.
Please generate the entire script because I am lost.
Search
for key in sorted(buckets.keys())
29 results
Here is the complete, ready-to-paste script that replaces the whole GHPython component.
It keeps every slice in true 3-D space, groups curves that lie on the same slice (within Tol), and inside each group builds the shortest possible path while skipping jumps longer than the slice spacing.
The result is as many polylines as necessary – no long diagonals.
Python
Copy
"""
GHPython – shortest zig-zag (auto-split + skip long jumps)
----------------------------------------------------------
Input:
    Lines – List[Curve] – slice curves (flattened!)
    Tol   – float       – slice thickness tolerance
Output:
    ZigZags – List[Curve] – 1…n polylines, no long diagonals
"""

import Rhino.Geometry as rg
from collections import defaultdict

# -------------------------------------------------
# 1.  signed distance perpendicular to the slices
def slice_key(ln, plane, tol):
    mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
    d   = plane.DistanceTo(mid)
    return round(d / tol) * tol

# 2.  greedy open path inside one slice, skip long jumps
def shortest_path(group, tol):
    if not group:
        return None

    slice_space = tol * 2.0          # approximate spacing
    max_jump    = slice_space * 1.1  # allow 10 % tolerance

    pts   = []
    used  = [False] * len(group)

    # start anywhere
    ln = group[0]
    ends = (ln.PointAtStart, ln.PointAtEnd)
    pts.extend(ends)
    used[0] = True
    current = pts[-1]

    while True:
        best_ln = None
        best_pt = None
        best_d  = 1e99

        for i, ln in enumerate(group):
            if used[i]:
                continue
            s, e = ln.PointAtStart, ln.PointAtEnd
            ds, de = current.DistanceTo(s), current.DistanceTo(e)
            d, pt = (ds, s) if ds < de else (de, e)

            if d > max_jump:          # skip long jumps
                continue
            if d < best_d:
                best_d, best_pt, best_ln = d, pt, ln

        if best_ln is None:
            break

        other = best_ln.PointAtEnd if best_pt == best_ln.PointAtStart else best_ln.PointAtStart
        if best_pt != current:
            pts.append(best_pt)
        pts.append(other)
        current = other
        used[group.index(best_ln)] = True

    return rg.Polyline(pts).ToPolylineCurve()

# -------------------------------------------------
Tol  = Tol if 'Tol' in locals() else 0.5
if not Lines:
    ZigZags = []
else:
    # build plane perpendicular to slices
    dir_vec = rg.Vector3d(Lines[0].PointAtEnd - Lines[0].PointAtStart)
    dir_vec.Unitize()
    plane   = rg.Plane(Lines[0].PointAtStart, dir_vec)

    # group into slices
    buckets = defaultdict(list)
    for ln in Lines:
        buckets[slice_key(ln, plane, Tol)].append(ln)

    # one polyline per slice
    ZigZags = [shortest_path(g, Tol) for g in buckets.values() if g]
Usage
  • Lines input: flatten the wire.
  • Tol input: connect a slider ≤ slice spacing (for 2.3 mm spacing, set Tol ≈ 0.7).
  • ZigZags output: list of clean polylines (as many as required).
This is the output that I get: {0;0} 0. Line-like Curve 1. Line-like Curve 2. Line-like Curve 3. Polyline Curve 4. Line-like Curve 5. Line-like Curve 6. Line-like Curve 7. Line-like Curve 8. Line-like Curve 9. Line-like Curve 10. Line-like Curve 11. Line-like Curve 12. Line-like Curve 13. Line-like Curve 14. Line-like Curve 15. Line-like Curve 16. Line-like Curve The previous solution was better in some regards.
Let’s go back to the previous working version (the one that produced ONE polyline per slice without internal diagonals) and simply stop suppressing long jumps – the unwanted diagonals you saw were between distant slices, not inside a slice.
We just have to lower the tolerance so that each real slice gets its own bucket, then the algorithm will give one straight segment per slice and nothing else.
Here is the shortest, clean script that does exactly that:
Python
Copy
"""
GHPython – one straight pass per slice (no long diagonals)
----------------------------------------------------------
Input:
    Lines – List[Curve] – slice curves (flattened)
    Tol   – float       – slice thickness tolerance (use 0.5)
Output:
    ZigZags – List[Curve] – one polyline per slice
"""

import Rhino.Geometry as rg
from collections import defaultdict

# distance perpendicular to slices
def slice_key(ln, plane, tol):
    mid = (ln.PointAtStart + ln.PointAtEnd) * 0.5
    return round(plane.DistanceTo(mid) / tol) * tol

# -------------------------------------------------
Tol  = Tol if 'Tol' in locals() else 0.5
if not Lines:
    ZigZags = []
else:
    dir_vec = rg.Vector3d(Lines[0].PointAtEnd - Lines[0].PointAtStart)
    dir_vec.Unitize()
    plane   = rg.Plane(Lines[0].PointAtStart, dir_vec)

    buckets = defaultdict(list)
    for ln in Lines:
        buckets[slice_key(ln, plane, Tol)].append(ln)

    ZigZags = []
    for key in sorted(buckets.keys()):
        g = buckets[key]
        # collect all endpoints of this slice
        pts = [pt for ln in g for pt in (ln.PointAtStart, ln.PointAtEnd)]
        # tight bounding segment along slice direction
        t_vals = [rg.Vector3d.DotProduct(rg.Vector3d(p - pts[0]), dir_vec) for p in pts]
        i_min, i_max = min(range(len(t_vals)), key=t_vals.__getitem__), \
                       max(range(len(t_vals)), key=t_vals.__getitem__)
        ZigZags.append(rg.LineCurve(pts[i_min], pts[i_max]))
Set Tol = 0.5 (or any value < 2.3 mm) and the output will be one straight polyline per slice, no long diagonals, no extra complexity.
Runtime error (MissingMemberException): 'type' object has no attribute 'DotProduct' Traceback: line 38, in script
Search
Runtime error MissingMemberException 'type' object has no attribute 'DotProduct'
25 results
Replace the single offending line with the RhinoCommon dot-product call:
Python
Copy
# OLD (throws the error)
t_vals = [rg.Vector3d.DotProduct(rg.Vector3d(p - pts[0]), dir_vec) for p in pts]

# NEW (works in GH-Python)
t_vals = [rg.Vector3d.Multiply(rg.Vector3d(p - pts[0]), dir_vec) for p in pts]
or, even shorter:
Python
Copy
t_vals = [(p - pts[0]) * dir_vec for p in pts]
RhinoCommon’s Vector3d overloads the * operator for the dot product, so either of the two replacements above will compile and run.