DEV Community
Godot 4: evenly spaced points along a path (77 stitch holes)
Placing points every N units along a polyline looks trivial in Godot 4 until the path bends. We hit this in a leathercraft game, where the points are stitch holes. On a rounded corner the holes bunched up, and on one seam two holes landed on the same spot. The naive version A seam is a polyline in metres. The player picks a pricking-iron pitch: 3.0, 3.38 or 3.85 mm. Our first sampler walked the polyline segment by segment: var gap: float = pitch_mm * .001
for i in range(seam.path.size()-1):
var a := vec2(seam.path[i])
var b := vec2(seam.path[i+1])
var count := maxi(1, roundi(a.distance_to(b) / gap))
for j in count:
var p := a.lerp(b, float(j)/count)
seam.holes.append([p.x, p.y])
var end := vec2(seam.path.back())
seam.holes.append([end.x,end.y])
Three things go wrong. Rounding happens per segment. A 10 mm segment and a 90 mm segment each round to their own count, so the spacing changes at every vertex. Curves are many short segments. A corner arc is built from segments shorter than the pitch. maxi(1, …) gives each of them a hole, so the corner gets a hole per vertex instead of a hole per 3.38 mm. Duplicate join points. Where two path pieces meet, the same point can appear twice. That is a zero-length segment, which still gets one hole. Two holes now sit on the same spot, and the direction from one hole to the next is a zero vector. Our needle used that direction to orient itself. Sample by arc length instead The fix is to stop thinking in segments. Measure the whole path once, decide the hole count once, then walk the path and drop a hole each time the walked distance passes the next target. var gap: float = pitch_mm * .001
var total := 0.0
for i in range(seam.path.size()-1):
total += vec2(seam.path[i]).distance_to(vec2(seam.path[i+1]))
var count := maxi(1, roundi(total/gap))
var walked := 0.0
var next_distance := 0.0
var hole := 0
for i in range(seam.path.size()-1):
var a := vec2(seam.path[i])
var b := vec2(seam.path[i+1])
var length := a.distance_to(b)
if length < .000001: continue
while hole <= count and next_distance <= walked+length+.0000001:
var p := a.lerp(b, clampf((next_distance-walked)/length, 0, 1))
seam.holes.append([p.x, p.y])
hole += 1
next_distance = totalfloat(hole)/count
walked += length
(The real function also handles an empty path; that line is left out here.) A few details that matter: count is the number of gaps, not holes. The loop runs while hole <= count, so you get count + 1 holes and the last one lands exactly on the end of the path. The real spacing is total / count, not the nominal pitch. A 100 mm seam at 3.38 mm rounds to 30 gaps, so the holes sit 3.333 mm apart. Nobody sees 0.05 mm, but everybody sees a half-width gap at the end of a seam. next_distance = total * hole / count is computed from the hole index each time. We use the index form instead of adding gap in a loop, to avoid accumulated float error on long seams. Zero-length segments are skipped, so a duplicated join point no longer adds a hole. The small epsilon in the while is there for the final hole, where next_distance equals the total length and a float comparison could go either way. This is distance along straight segments, so it is exact for a polyline. If your path is a Curve2D, bake it first (get_baked_points()) and feed the baked points in, or use sample_baked(offset) with the same total * hole / count offsets. Test the ugly case The test that guards this is a path with a deliberately repeated point: var joined := {"path":[[0,0],[.01,0],[.01,0],[.1,0]],
"style":{"pitch_mm":3.38},"holes":[]}
screen._sample_holes(joined)
for i in range(1, joined.holes.size()):
var d := screen.vec2(joined.holes[i]).distance_to(screen.vec2(joined.holes[i-1]))
if absf(d - .1/30) > .000001:
failures.append("Repeated curve joins must not add holes or change the stitch spacing")
Every neighbouring pair must be 0.1 / 30 m apart, to a micr...
Powered by Readybot.io*
External Link
Platform
You are about to visit:
https://example.comThis will open in a new tab. Make sure you trust this link.