Path Operations
Hobby provides operations to query and manipulate paths.
Path Queries
pointat(t)
Returns the point at parameter t on the path.
local p = path:pointat(0.5)
print(p.x, p.y)Parameter t ranges from 0 to path.length. Integer values correspond to knot points.
directionat(t)
Returns the tangent direction at parameter t.
local dir = path:directionat(0.5)directiontime(dx, dy)
Finds the first parameter t where the path has the given tangent direction. Returns -1 if the direction is never achieved.
-- Find where tangent is horizontal (pointing right)
local t = path:directiontime(1, 0)
-- Find where tangent is at 45 degrees
local t45 = path:directiontime(1, 1)directionpoint(dx, dy)
Returns the first point where the path has the given tangent direction. Returns nil if the direction is never achieved.
local pt = path:directionpoint(1, 0) -- point with horizontal tangent
if pt then
print(string.format("Horizontal at (%.1f, %.1f)", pt.x, pt.y))
endprecontrol(t) / postcontrol(t)
Returns the Bézier control points at parameter t.
local pre = path:precontrol(1) -- incoming control
local post = path:postcontrol(1) -- outgoing controllength
Number of segments in the path.
print(path.length) -- e.g., 4 for a squarearclength
Total arc length of the path.
local len = path.arclength
print(string.format("Arc length: %.2f", len))arctime(length)
Returns the parameter t where the arc length reaches the given value.
local t = path:arctime(path.arclength / 2) -- midpoint by arc length
local midpoint = path:pointat(t)Path Manipulation
subpath(t1, t2)
Extracts a portion of the path between parameters t1 and t2.
local sub = path:subpath(0.5, 2.5)reversed()
Returns the path with reversed direction.
local rev = path:reversed()cutbefore(other)
Returns the portion of the path after its first intersection with another path. If there is no intersection, returns a copy of the original path.
local after_intersection = path1:cutbefore(path2)cutafter(other)
Returns the portion of the path before its first intersection with another path. If there is no intersection, returns a copy of the original path.
local before_intersection = path1:cutafter(path2)Example combining both:
-- Split a path at its intersection with a line
local line = h.path()
:moveto(h.point(0, 50))
:lineto(h.point(100, 50))
:build()
local curve = h.path()
:moveto(h.point(0, 0))
:curveto(h.point(50, 100))
:curveto(h.point(100, 0))
:build()
local part1 = curve:cutafter(line) -- curve from start to intersection
local part2 = curve:cutbefore(line) -- curve from intersection to endIntersections
intersectiontimes(other)
Finds the first intersection between two paths. Returns the parameter values on each path.
local t1, t2 = path1:intersectiontimes(path2)
if t1 then
print(string.format("Intersection at t1=%.3f, t2=%.3f", t1, t2))
endintersectionpoint(other)
Returns the intersection point directly.
local p = path1:intersectionpoint(path2)
if p then
print(string.format("Intersection at (%.2f, %.2f)", p.x, p.y))
endBuilding Regions
buildcycle(path1, path2, …)
Constructs a closed region from multiple paths.
local h = require("hobby")
-- Two overlapping circles
local circle1 = h.fullcircle():scaled(60):shifted(30, 50)
local circle2 = h.fullcircle():scaled(60):shifted(70, 50)
-- Build the lens-shaped intersection
local lens = h.buildcycle(circle1, circle2)
if lens then
lens = lens:fill("yellow"):stroke("black")
endComplete Example
local h = require("hobby")
-- Create a curved path
local curve = h.path()
:moveto(h.point(0, 0))
:curveto(h.point(50, 80))
:curveto(h.point(100, 0))
:stroke("blue")
:strokewidth(2)
:build()
-- Query arc length
print(string.format("Arc length: %.2f", curve.arclength))
-- Find midpoint by arc length
local mid_t = curve:arctime(curve.arclength / 2)
local mid_p = curve:pointat(mid_t)
print(string.format("Midpoint: (%.1f, %.1f)", mid_p.x, mid_p.y))
-- Mark points at regular intervals
local marks = {}
for i = 0, 8 do
local t = curve:arctime(curve.arclength * i / 8)
local p = curve:pointat(t)
local mark = h.fullcircle()
:scaled(4)
:shifted(p.x, p.y)
:fill("red")
table.insert(marks, mark)
end
-- Create intersection example
local line = h.path()
:moveto(h.point(0, 40))
:lineto(h.point(100, 40))
:evenly()
:stroke("gray")
:build()
local t1, t2 = curve:intersectiontimes(line)
local intersection = curve:intersectionpoint(line)
local cross_mark = h.fullcircle()
:scaled(6)
:shifted(intersection.x, intersection.y)
:fill("green")
-- Output
local svg = h.svg():padding(10):add(curve):add(line):add(cross_mark)
for _, m in ipairs(marks) do
svg:add(m)
end
svg:write("pathops.svg")
print("Created pathops.svg")