initial commit
This commit is contained in:
@@ -0,0 +1,554 @@
|
||||
package pdf
|
||||
|
||||
import "core:math"
|
||||
|
||||
// Path_Verb tags one element of a path. The point count each verb consumes is
|
||||
// fixed: Move and Line take one, Curve takes three (two controls and an end
|
||||
// point), Close takes none.
|
||||
Path_Verb :: enum u8 {
|
||||
Move,
|
||||
Line,
|
||||
Curve,
|
||||
Close,
|
||||
}
|
||||
|
||||
// Path is a sequence of subpaths in one coordinate space.
|
||||
//
|
||||
// Verbs and points are kept in parallel arrays rather than as a tagged union
|
||||
// per element: a page of text is tens of thousands of elements, and the
|
||||
// rasteriser walks them once per fill, so the layout that keeps points
|
||||
// contiguous is worth the small awkwardness of the cursor arithmetic.
|
||||
Path :: struct {
|
||||
verbs: [dynamic]Path_Verb,
|
||||
points: [dynamic]Point,
|
||||
// Start of the current subpath, for Close.
|
||||
start: Point,
|
||||
// Current point. Meaningless before the first Move.
|
||||
cur: Point,
|
||||
open: bool,
|
||||
}
|
||||
|
||||
path_make :: proc(allocator := context.allocator, cap := 32) -> Path {
|
||||
return Path{
|
||||
verbs = make([dynamic]Path_Verb, 0, cap, allocator),
|
||||
points = make([dynamic]Point, 0, cap, allocator),
|
||||
}
|
||||
}
|
||||
|
||||
path_destroy :: proc(p: ^Path) {
|
||||
if p == nil { return }
|
||||
delete(p.verbs)
|
||||
delete(p.points)
|
||||
p^ = {}
|
||||
}
|
||||
|
||||
path_clear :: proc(p: ^Path) {
|
||||
clear(&p.verbs)
|
||||
clear(&p.points)
|
||||
p.open = false
|
||||
}
|
||||
|
||||
path_is_empty :: proc(p: ^Path) -> bool {
|
||||
return p == nil || len(p.verbs) == 0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Construction
|
||||
//
|
||||
// Every entry point drops a non-finite coordinate rather than storing it. A
|
||||
// single NaN reaching the rasteriser turns a fill into an unbounded scanline
|
||||
// walk, and content streams contain NaNs -- usually from a division by a zero
|
||||
// text size -- often enough that filtering here is cheaper than checking in
|
||||
// the inner loop.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
path_move_to :: proc(p: ^Path, x, y: f64) {
|
||||
if !is_finite(x) || !is_finite(y) { return }
|
||||
append(&p.verbs, Path_Verb.Move)
|
||||
append(&p.points, Point{x, y})
|
||||
p.start = Point{x, y}
|
||||
p.cur = Point{x, y}
|
||||
p.open = true
|
||||
}
|
||||
|
||||
path_line_to :: proc(p: ^Path, x, y: f64) {
|
||||
if !is_finite(x) || !is_finite(y) { return }
|
||||
// A line with no current point starts a subpath instead of being dropped:
|
||||
// content streams do emit "l" before "m", and viewers treat it as a move.
|
||||
if !p.open {
|
||||
path_move_to(p, x, y)
|
||||
return
|
||||
}
|
||||
append(&p.verbs, Path_Verb.Line)
|
||||
append(&p.points, Point{x, y})
|
||||
p.cur = Point{x, y}
|
||||
}
|
||||
|
||||
path_curve_to :: proc(p: ^Path, x1, y1, x2, y2, x3, y3: f64) {
|
||||
if !is_finite(x1) || !is_finite(y1) || !is_finite(x2) ||
|
||||
!is_finite(y2) || !is_finite(x3) || !is_finite(y3) {
|
||||
return
|
||||
}
|
||||
if !p.open {
|
||||
path_move_to(p, x1, y1)
|
||||
}
|
||||
append(&p.verbs, Path_Verb.Curve)
|
||||
append(&p.points, Point{x1, y1}, Point{x2, y2}, Point{x3, y3})
|
||||
p.cur = Point{x3, y3}
|
||||
}
|
||||
|
||||
// path_curve_to_v is the "v" operator: the first control point is the current
|
||||
// point.
|
||||
path_curve_to_v :: proc(p: ^Path, x2, y2, x3, y3: f64) {
|
||||
path_curve_to(p, p.cur.x, p.cur.y, x2, y2, x3, y3)
|
||||
}
|
||||
|
||||
// path_curve_to_y is the "y" operator: the second control point is the end
|
||||
// point.
|
||||
path_curve_to_y :: proc(p: ^Path, x1, y1, x3, y3: f64) {
|
||||
path_curve_to(p, x1, y1, x3, y3, x3, y3)
|
||||
}
|
||||
|
||||
path_close :: proc(p: ^Path) {
|
||||
if !p.open || len(p.verbs) == 0 { return }
|
||||
if p.verbs[len(p.verbs) - 1] == .Close { return }
|
||||
append(&p.verbs, Path_Verb.Close)
|
||||
p.cur = p.start
|
||||
}
|
||||
|
||||
// path_rect appends a closed rectangle, in the winding direction the "re"
|
||||
// operator specifies (counter-clockwise in user space).
|
||||
path_rect :: proc(p: ^Path, x, y, w, h: f64) {
|
||||
if !is_finite(x) || !is_finite(y) || !is_finite(w) || !is_finite(h) { return }
|
||||
path_move_to(p, x, y)
|
||||
path_line_to(p, x + w, y)
|
||||
path_line_to(p, x + w, y + h)
|
||||
path_line_to(p, x, y + h)
|
||||
path_close(p)
|
||||
}
|
||||
|
||||
// path_ellipse appends a closed ellipse inscribed in the rectangle, built from
|
||||
// four cubics. The magic constant is the standard circle-to-cubic ratio.
|
||||
path_ellipse :: proc(p: ^Path, r: Rect) {
|
||||
K :: 0.5522847498307933
|
||||
cx := (r.x0 + r.x1) * 0.5
|
||||
cy := (r.y0 + r.y1) * 0.5
|
||||
rx := (r.x1 - r.x0) * 0.5
|
||||
ry := (r.y1 - r.y0) * 0.5
|
||||
ox := rx * K
|
||||
oy := ry * K
|
||||
path_move_to(p, cx + rx, cy)
|
||||
path_curve_to(p, cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry)
|
||||
path_curve_to(p, cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy)
|
||||
path_curve_to(p, cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry)
|
||||
path_curve_to(p, cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy)
|
||||
path_close(p)
|
||||
}
|
||||
|
||||
// path_append copies every element of `src` onto `dst`.
|
||||
path_append :: proc(dst: ^Path, src: ^Path) {
|
||||
if src == nil { return }
|
||||
append(&dst.verbs, ..src.verbs[:])
|
||||
append(&dst.points, ..src.points[:])
|
||||
dst.cur = src.cur
|
||||
dst.start = src.start
|
||||
dst.open = src.open || dst.open
|
||||
}
|
||||
|
||||
// path_append_transformed copies `src` onto `dst` with `m` applied.
|
||||
path_append_transformed :: proc(dst: ^Path, src: ^Path, m: Matrix) {
|
||||
if src == nil { return }
|
||||
for pt in src.points {
|
||||
append(&dst.points, transform_point(m, pt))
|
||||
}
|
||||
append(&dst.verbs, ..src.verbs[:])
|
||||
dst.cur = transform_point(m, src.cur)
|
||||
dst.start = transform_point(m, src.start)
|
||||
dst.open = src.open || dst.open
|
||||
}
|
||||
|
||||
path_transform :: proc(p: ^Path, m: Matrix) {
|
||||
for &pt in p.points {
|
||||
pt = transform_point(m, pt)
|
||||
}
|
||||
p.cur = transform_point(m, p.cur)
|
||||
p.start = transform_point(m, p.start)
|
||||
}
|
||||
|
||||
path_clone :: proc(p: ^Path, allocator := context.allocator) -> Path {
|
||||
out := path_make(allocator, max(len(p.verbs), 8))
|
||||
append(&out.verbs, ..p.verbs[:])
|
||||
append(&out.points, ..p.points[:])
|
||||
out.cur = p.cur
|
||||
out.start = p.start
|
||||
out.open = p.open
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bounds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// path_bounds is the control-point hull, which is a conservative
|
||||
// over-estimate for curves: a cubic never leaves the convex hull of its four
|
||||
// points. Good enough for culling; `path_tight_bounds` is the exact one.
|
||||
path_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect {
|
||||
if p == nil || len(p.points) == 0 { return RECT_EMPTY }
|
||||
identity := matrix_is_identity(m)
|
||||
first := identity ? p.points[0] : transform_point(m, p.points[0])
|
||||
r := Rect{first.x, first.y, first.x, first.y}
|
||||
for i in 1 ..< len(p.points) {
|
||||
pt := identity ? p.points[i] : transform_point(m, p.points[i])
|
||||
r.x0 = min(r.x0, pt.x)
|
||||
r.y0 = min(r.y0, pt.y)
|
||||
r.x1 = max(r.x1, pt.x)
|
||||
r.y1 = max(r.y1, pt.y)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// path_tight_bounds solves for the cubic's extrema instead of using the hull.
|
||||
// Worth it for the clip stack, where a loose box costs whole scanlines.
|
||||
path_tight_bounds :: proc(p: ^Path, m := MATRIX_IDENTITY) -> Rect {
|
||||
if p == nil || len(p.verbs) == 0 { return RECT_EMPTY }
|
||||
|
||||
r := RECT_EMPTY
|
||||
have := false
|
||||
include :: proc(r: ^Rect, have: ^bool, pt: Point) {
|
||||
if !have^ {
|
||||
r^ = Rect{pt.x, pt.y, pt.x, pt.y}
|
||||
have^ = true
|
||||
return
|
||||
}
|
||||
r.x0 = min(r.x0, pt.x)
|
||||
r.y0 = min(r.y0, pt.y)
|
||||
r.x1 = max(r.x1, pt.x)
|
||||
r.y1 = max(r.y1, pt.y)
|
||||
}
|
||||
|
||||
cur := Point{}
|
||||
i := 0
|
||||
for verb in p.verbs {
|
||||
switch verb {
|
||||
case .Move, .Line:
|
||||
if i >= len(p.points) { break }
|
||||
pt := transform_point(m, p.points[i])
|
||||
include(&r, &have, pt)
|
||||
cur = pt
|
||||
i += 1
|
||||
case .Curve:
|
||||
if i + 2 >= len(p.points) { break }
|
||||
c1 := transform_point(m, p.points[i])
|
||||
c2 := transform_point(m, p.points[i + 1])
|
||||
e := transform_point(m, p.points[i + 2])
|
||||
include(&r, &have, e)
|
||||
cubic_extrema_include(&r, &have, cur, c1, c2, e)
|
||||
cur = e
|
||||
i += 3
|
||||
case .Close:
|
||||
// no points
|
||||
}
|
||||
}
|
||||
return have ? r : RECT_EMPTY
|
||||
}
|
||||
|
||||
// cubic_extrema_include folds the interior extrema of one cubic into the box.
|
||||
// The derivative of a cubic Bezier is a quadratic; its roots in (0,1) are the
|
||||
// only places the curve can turn.
|
||||
@(private)
|
||||
cubic_extrema_include :: proc(r: ^Rect, have: ^bool, p0, p1, p2, p3: Point) {
|
||||
for axis in 0 ..< 2 {
|
||||
v0 := p0[axis]
|
||||
v1 := p1[axis]
|
||||
v2 := p2[axis]
|
||||
v3 := p3[axis]
|
||||
|
||||
a := -v0 + 3 * v1 - 3 * v2 + v3
|
||||
b := 2 * (v0 - 2 * v1 + v2)
|
||||
c := v1 - v0
|
||||
|
||||
fold :: proc(r: ^Rect, have: ^bool, axis: int, value: f64) {
|
||||
if !is_finite(value) { return }
|
||||
if !have^ {
|
||||
r^ = Rect{value, value, value, value}
|
||||
have^ = true
|
||||
return
|
||||
}
|
||||
if axis == 0 {
|
||||
r.x0 = min(r.x0, value)
|
||||
r.x1 = max(r.x1, value)
|
||||
} else {
|
||||
r.y0 = min(r.y0, value)
|
||||
r.y1 = max(r.y1, value)
|
||||
}
|
||||
}
|
||||
|
||||
eval :: proc(v0, v1, v2, v3, t: f64) -> f64 {
|
||||
mt := 1 - t
|
||||
return mt * mt * mt * v0 + 3 * mt * mt * t * v1 + 3 * mt * t * t * v2 + t * t * t * v3
|
||||
}
|
||||
|
||||
// Degenerate to a linear derivative: solve b*t + c == 0.
|
||||
if abs(a) < 1e-12 {
|
||||
if abs(b) > 1e-12 {
|
||||
t := -c / b
|
||||
if t > 0 && t < 1 {
|
||||
fold(r, have, axis, eval(v0, v1, v2, v3, t))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 3*a*t^2 + 2*b'*t + 3*c == 0, with the constant factors cancelled.
|
||||
disc := b * b - 4 * a * c
|
||||
if disc < 0 { continue }
|
||||
sq := math.sqrt(disc)
|
||||
for t in ([2]f64{(-b + sq) / (2 * a), (-b - sq) / (2 * a)}) {
|
||||
if t > 0 && t < 1 {
|
||||
fold(r, have, axis, eval(v0, v1, v2, v3, t))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flattening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Flat_Subpath describes one polyline inside a Flat_Path.
|
||||
Flat_Subpath :: struct {
|
||||
start: int, // index into Flat_Path.points
|
||||
count: int,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
// Flat_Path is a path reduced to polylines, in device space. The rasteriser
|
||||
// and the stroker both consume this rather than curves.
|
||||
//
|
||||
// Subpaths are described explicitly rather than by a start-index array with a
|
||||
// sentinel, so that several paths can be flattened into one Flat_Path -- which
|
||||
// is exactly what a clip stack and a tiling pattern need to do.
|
||||
Flat_Path :: struct {
|
||||
points: [dynamic]Point,
|
||||
subpaths: [dynamic]Flat_Subpath,
|
||||
}
|
||||
|
||||
flat_path_make :: proc(allocator := context.allocator) -> Flat_Path {
|
||||
return Flat_Path{
|
||||
points = make([dynamic]Point, 0, 64, allocator),
|
||||
subpaths = make([dynamic]Flat_Subpath, 0, 8, allocator),
|
||||
}
|
||||
}
|
||||
|
||||
flat_path_destroy :: proc(fp: ^Flat_Path) {
|
||||
if fp == nil { return }
|
||||
delete(fp.points)
|
||||
delete(fp.subpaths)
|
||||
fp^ = {}
|
||||
}
|
||||
|
||||
flat_path_clear :: proc(fp: ^Flat_Path) {
|
||||
clear(&fp.points)
|
||||
clear(&fp.subpaths)
|
||||
}
|
||||
|
||||
flat_path_count :: proc(fp: ^Flat_Path) -> int {
|
||||
return len(fp.subpaths)
|
||||
}
|
||||
|
||||
flat_path_slice :: proc(fp: ^Flat_Path, i: int) -> []Point {
|
||||
if i < 0 || i >= len(fp.subpaths) { return nil }
|
||||
sp := fp.subpaths[i]
|
||||
if sp.start < 0 || sp.start + sp.count > len(fp.points) { return nil }
|
||||
return fp.points[sp.start:sp.start + sp.count]
|
||||
}
|
||||
|
||||
flat_path_bounds :: proc(fp: ^Flat_Path) -> Rect {
|
||||
if fp == nil || len(fp.points) == 0 { return RECT_EMPTY }
|
||||
r := Rect{fp.points[0].x, fp.points[0].y, fp.points[0].x, fp.points[0].y}
|
||||
for pt in fp.points {
|
||||
r.x0 = min(r.x0, pt.x)
|
||||
r.y0 = min(r.y0, pt.y)
|
||||
r.x1 = max(r.x1, pt.x)
|
||||
r.y1 = max(r.y1, pt.y)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FLATNESS is the maximum distance, in device pixels, between the true curve
|
||||
// and the polyline that replaces it. A quarter of a pixel is below what
|
||||
// anti-aliased output can show and keeps segment counts sane on text.
|
||||
FLATNESS :: 0.25
|
||||
|
||||
// path_flatten transforms `p` by `m` and subdivides every curve into line
|
||||
// segments, appending the result to `out`. Existing contents of `out` are kept.
|
||||
//
|
||||
// Subdivision is adaptive and depth-limited: a curve with a cusp or a very
|
||||
// large control polygon would otherwise subdivide until the stack ran out.
|
||||
//
|
||||
// `cull`, when given, is the region -- in the same space `m` maps into -- that
|
||||
// the result will actually be drawn or clipped through. A curve that cannot
|
||||
// reach it is replaced by its chord instead of being subdivided, which is what
|
||||
// keeps a path whose control points are a billion units off the page from
|
||||
// costing a million points per painting operator. See `flatten_reaches`: the
|
||||
// substitution is exact, not an approximation.
|
||||
path_flatten :: proc(out: ^Flat_Path, p: ^Path, m: Matrix, flatness := FLATNESS,
|
||||
cull: Maybe(Rect) = nil) {
|
||||
if out == nil || p == nil || len(p.verbs) == 0 { return }
|
||||
|
||||
tol := max(flatness, 0.01)
|
||||
cull_box, culling := cull.?
|
||||
cur := Point{}
|
||||
i := 0
|
||||
// Index into out.subpaths of the subpath being built, or -1.
|
||||
active := -1
|
||||
|
||||
for verb in p.verbs {
|
||||
if len(out.points) >= MAX_FLATTEN_POINTS { break }
|
||||
switch verb {
|
||||
case .Move:
|
||||
if i >= len(p.points) { break }
|
||||
cur = transform_point(m, p.points[i])
|
||||
i += 1
|
||||
append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1})
|
||||
append(&out.points, cur)
|
||||
active = len(out.subpaths) - 1
|
||||
case .Line:
|
||||
if i >= len(p.points) { break }
|
||||
pt := transform_point(m, p.points[i])
|
||||
i += 1
|
||||
if active < 0 {
|
||||
// A segment after a close begins a new subpath AT the current
|
||||
// point, which `h` left at the start of the subpath it closed
|
||||
// (ISO 32000-2, 8.5.2.1). Seeding it is what keeps the
|
||||
// connecting segment: without `cur` the new subpath is a
|
||||
// single stray point, which a fill drops and a stroke turns
|
||||
// into a dot in the wrong place. The .Curve arm below has
|
||||
// always done this; the two must agree.
|
||||
append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1})
|
||||
append(&out.points, cur)
|
||||
active = len(out.subpaths) - 1
|
||||
}
|
||||
append(&out.points, pt)
|
||||
out.subpaths[active].count += 1
|
||||
cur = pt
|
||||
case .Curve:
|
||||
if i + 2 >= len(p.points) { break }
|
||||
c1 := transform_point(m, p.points[i])
|
||||
c2 := transform_point(m, p.points[i + 1])
|
||||
e := transform_point(m, p.points[i + 2])
|
||||
i += 3
|
||||
if active < 0 {
|
||||
append(&out.subpaths, Flat_Subpath{start = len(out.points), count = 1})
|
||||
append(&out.points, cur)
|
||||
active = len(out.subpaths) - 1
|
||||
}
|
||||
before := len(out.points)
|
||||
flatten_cubic(&out.points, cur, c1, c2, e, tol, 0, cull_box, culling)
|
||||
append(&out.points, e)
|
||||
out.subpaths[active].count += len(out.points) - before
|
||||
cur = e
|
||||
case .Close:
|
||||
if active >= 0 {
|
||||
out.subpaths[active].closed = true
|
||||
sp := out.subpaths[active]
|
||||
if sp.count > 0 {
|
||||
cur = out.points[sp.start]
|
||||
}
|
||||
}
|
||||
// A subpath after a close starts fresh, but at the same point:
|
||||
// "m 0 0 l 10 0 h l 20 0" continues from the start point.
|
||||
active = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MAX_FLATTEN_DEPTH bounds recursion at 2^16 segments per curve, which no
|
||||
// legitimate curve approaches and a degenerate one would exceed forever.
|
||||
MAX_FLATTEN_DEPTH :: 16
|
||||
|
||||
// MAX_FLATTEN_POINTS bounds the whole of one flattening. MAX_FLATTEN_DEPTH caps
|
||||
// a SINGLE curve, and nothing capped the number of curves: a content stream may
|
||||
// append hundreds of thousands of `c` segments to one path before the painting
|
||||
// operator that clears it, and a cubic whose control points are a billion units
|
||||
// off the chord subdivides to the depth limit every time. One `f` could
|
||||
// therefore ask for gigabytes of polyline -- `rd_fill_path` materialises the
|
||||
// whole flattened path before `raster_fill` ever sees the clip that would have
|
||||
// discarded it. A million points is two orders of magnitude past the densest
|
||||
// real path and costs 16 MB; past it the geometry is truncated, which draws
|
||||
// part of a shape no viewer could have drawn all of.
|
||||
MAX_FLATTEN_POINTS :: 1 << 20
|
||||
|
||||
// flatten_reaches reports whether any part of the cubic can lie inside `box`.
|
||||
//
|
||||
// Every point of a Bezier lies inside the convex hull of its control points,
|
||||
// so a hull that misses the box means the whole curve does. It is used to stop
|
||||
// subdividing there, which leaves the chord between the curve's ends in its
|
||||
// place -- and that chord is inside the same hull. Deforming one to the other
|
||||
// therefore never crosses a point of `box`, so the winding number, and with it
|
||||
// both fill rules, is unchanged everywhere the box covers. The picture inside
|
||||
// the clip is exactly the picture the full subdivision would have drawn.
|
||||
//
|
||||
// A non-finite coordinate makes every comparison false and the curve is kept,
|
||||
// which is the same thing the un-culled path does with it.
|
||||
@(private)
|
||||
flatten_reaches :: proc(p0, p1, p2, p3: Point, box: Rect) -> bool {
|
||||
if max(p0.x, p1.x, p2.x, p3.x) < box.x0 { return false }
|
||||
if min(p0.x, p1.x, p2.x, p3.x) > box.x1 { return false }
|
||||
if max(p0.y, p1.y, p2.y, p3.y) < box.y0 { return false }
|
||||
if min(p0.y, p1.y, p2.y, p3.y) > box.y1 { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@(private)
|
||||
flatten_cubic :: proc(out: ^[dynamic]Point, p0, p1, p2, p3: Point, tol: f64, depth: int,
|
||||
cull: Rect, culling: bool) {
|
||||
if depth >= MAX_FLATTEN_DEPTH || len(out) >= MAX_FLATTEN_POINTS {
|
||||
return
|
||||
}
|
||||
// Tested at every level, not only the first: the halves of a curve that
|
||||
// sweeps across the page and back are each smaller than the whole, so a
|
||||
// curve nothing culls at the top collapses to a handful of segments a few
|
||||
// levels down.
|
||||
if culling && !flatten_reaches(p0, p1, p2, p3, cull) {
|
||||
return
|
||||
}
|
||||
// Flatness test: how far the control points sit off the chord. d1 and d2
|
||||
// are perpendicular distances scaled by the chord length, so comparing
|
||||
// (d1+d2)^2 against tol^2 * chord^2 is the same as requiring the two
|
||||
// perpendicular distances to sum to less than `tol` -- and the curve's own
|
||||
// deviation from the chord is at most three quarters of that.
|
||||
dx := p3.x - p0.x
|
||||
dy := p3.y - p0.y
|
||||
chord2 := dx * dx + dy * dy
|
||||
|
||||
if chord2 < 1e-12 {
|
||||
// A curve that comes back to its own start point has no chord to
|
||||
// measure against, and the cross-product test would call it flat --
|
||||
// collapsing an entire loop into a zero-length segment. Measure the
|
||||
// control points against the start point instead.
|
||||
c1 := (p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)
|
||||
c2 := (p2.x - p0.x) * (p2.x - p0.x) + (p2.y - p0.y) * (p2.y - p0.y)
|
||||
if max(c1, c2) <= tol * tol {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
d1 := abs((p1.x - p3.x) * dy - (p1.y - p3.y) * dx)
|
||||
d2 := abs((p2.x - p3.x) * dy - (p2.y - p3.y) * dx)
|
||||
dd := d1 + d2
|
||||
if dd * dd <= tol * tol * chord2 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// de Casteljau split at t = 0.5.
|
||||
p01 := Point{(p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5}
|
||||
p12 := Point{(p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5}
|
||||
p23 := Point{(p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5}
|
||||
p012 := Point{(p01.x + p12.x) * 0.5, (p01.y + p12.y) * 0.5}
|
||||
p123 := Point{(p12.x + p23.x) * 0.5, (p12.y + p23.y) * 0.5}
|
||||
mid := Point{(p012.x + p123.x) * 0.5, (p012.y + p123.y) * 0.5}
|
||||
|
||||
flatten_cubic(out, p0, p01, p012, mid, tol, depth + 1, cull, culling)
|
||||
append(out, mid)
|
||||
flatten_cubic(out, mid, p123, p23, p3, tol, depth + 1, cull, culling)
|
||||
}
|
||||
Reference in New Issue
Block a user