Path Operations

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))
end

precontrol(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 control

length

Number of segments in the path.

print(path.length)  -- e.g., 4 for a square

arclength

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 end

Intersections

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))
end

intersectionpoint(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))
end

Building 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")
end

Complete 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")

Path operations example