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:
The idea is very simple:
- Sort the lines from bottom to top (by Y-coordinate of their start point).
- Walk through the sorted list, alternating the direction of travel (left-to-right, right-to-left, left-to-right …).
- Add the two endpoints of every line in that order to one big point list.
- 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 = NoneHow to use
- In Grasshopper, add a GHPython component.
- Paste the script above.
- Add an input called
Lines(type hint: Curve, list access). - (Optional) add a boolean input named
flipto quickly reverse direction when needed. - The output
ZigZagis 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.
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 = NoneWire 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:
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 = NoneDrop 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
- We project all lines onto the global XY-plane for sorting only; the real geometry is kept unchanged.
- We sort the lines by the signed distance along their common direction (not by Y).
- We give you a
Flipboolean 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
FlipatTruefor 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:
Instead of projecting them onto XY, we treat them in their own local frame:
- Take the direction of the first line as the sort axis (local X).
- Compute a perpendicular vector inside the same plane as the lines (local Y).
- Sort every line by its mid-point projected on that local X.
- 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.
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:
For a set of perfectly parallel but arbitrarily oriented lines we must:
- Build a local 2-D coordinate system defined by the slice plane.
- Map every line to that plane (so we always have local X and Y).
- Sort the lines by their local Y (the axis perpendicular to the lines).
- 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
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:
For any parallel orientation we must instead:
- Build the actual plane that contains all the slices.
- Turn every line into a 2-D segment in that plane.
- Sort those segments by their mid-point along the direction that is perpendicular to the lines (the “offset” direction).
- 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
No projection, no single-axis distance—only the true local 2-D coordinates of the slices.
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
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.
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.
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
- keeps the real 3-D geometry of the slices,
- never builds crossing diagonals, and
- 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.
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
Before we debug further, please double-check these three things:
Lines is empty in the GHPython component.Before we debug further, please double-check these three things:
- Type hint on the
Linesinput is Curve (not Line) and List Access is enabled. - Your wires actually carry geometry (you can add a temporary
Panelright before the GHPython input to see the count). - 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.
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
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.
• 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
That always happens when the input list is literally empty at the moment Grasshopper evaluates the component.
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:
- Connect a Panel directly to the wire that goes into the GHPython
Linesinput.
– Does the panel show any geometry?
– Does it say “0 items”? - If the panel shows curves, make sure the GHPython component has
• Type hint: Curve
• List Access: True
• Parameter name exactlyLines - 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
Flatten the wire before it enters
{0} with 67 items), while the GHPython component is expecting a flat list.Flatten the wire before it enters
Lines:- Right-click the
Linesinput → Flatten (or add a Flatten component in-between). - Make sure the panel (after Flatten) now showsplainCopy
0. Line-like Curve 1. Line-like Curve ... 66. Line-like Curvewithout 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.VectorOutput 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
Linesinput → flatten the wire (right-click → Flatten).Tolinput → distance that defines “same slice” (0.5 mm by default).ZigZagsoutput → 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 / 5So 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
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
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.
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 choiceor simply use
plain
Copy
Tol = 0.5 mmWith
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:
- Replace the constant in the script
Tol = 0.5(or expose it as an input and set it to 0.5). - 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.5means:
- 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:
- Make sure the GHPython component has an input called
Tol(type-hint Number, Item Access). - Connect a Number Slider set to 0.5 (or any value < slice-spacing).
- 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:
- Connect a Number Slider to the
Tolinput. - Set the slider range 0.1 – 1.0 (or narrower).
- Start at 0.5 → you should obtain ≈ 10–12 separate polylines and no diagonals between slices.
- 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.
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.
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 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:
Please do this one-minute check:
- Double-click the GHPython component → Inputs / Outputs tab.
- Make sure the output is literally named
ZigZags(plural, no extra spaces). - If it still says
ZigZag(singular) rename it toZigZagsand press OK. - 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.
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
The result is as many polylines as necessary – no long diagonals.
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
Linesinput: flatten the wire.Tolinput: connect a slider ≤ slice spacing (for 2.3 mm spacing, setTol ≈ 0.7).ZigZagsoutput: 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.
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.