package pdf import "core:math" // --------------------------------------------------------------------------- // Stroking (ISO 32000-2, 8.4.3) // // A stroke is converted into an outline that is then FILLED with the nonzero // rule. Even-odd would be wrong and not merely different: the two sides of a // stroke overlap wherever a join doubles back, wherever a path crosses itself // and wherever two dashes land on the same ink, and even-odd punches holes in // exactly those places. // // The outline is built in USER space and then transformed. Line width is a // user-space quantity (8.4.3.2), so an anisotropic CTM genuinely makes the pen // elliptical: under `1 0 0 4 0 0 cm` a horizontal line is painted four times // as thick as a vertical one of the same width. Scaling the width by // matrix_expansion and stroking in device space would paint both at twice the // width, which is wrong for both. // // Every contour emitted here is wound the same way (clockwise in build space, // and so consistently one way in device space whatever the CTM does), with the // single deliberate exception of the inner ring of a closed subpath, which is // wound the other way so the nonzero rule leaves the hole unpainted. The // consistency is load-bearing: two overlapping dash pieces wound oppositely // would cancel and leave a hole where there should be ink. // --------------------------------------------------------------------------- // HAIRLINE_WIDTH is the device-space width of the thinnest line the renderer // will draw. One pixel: "a line width of 0 shall denote the thinnest line that // can be rendered at device resolution" (8.4.3.2), and the same has to apply // to any width that would come out thinner, or the table rules, underlines and // form boxes that fill ordinary business documents vanish at screen scale. @(private) HAIRLINE_WIDTH :: 1.0 // MAX_ARC_SEGMENTS bounds one round join or cap, MAX_CIRCLE_SEGMENTS one dot. // The segment count is normally driven by the flatness tolerance; the cap only // bites for a pen thousands of pixels across, where the extra error is a // fraction of a percent of the radius. @(private) MAX_ARC_SEGMENTS :: 128 @(private) MAX_CIRCLE_SEGMENTS :: 256 // MAX_DASH_PIECES is the hard ceiling on dashes generated from one path, and // DASH_UNBOUNDED is the "rest of the path is solid" sentinel used once it is // hit. A finite sentinel rather than an infinity, because no infinity may // reach the geometry. @(private) MAX_DASH_PIECES :: 100_000 @(private) DASH_UNBOUNDED :: 1e30 // MAX_STROKE_POINTS bounds the whole outline. A path with a million segments // and round joins would otherwise turn into hundreds of megabytes of geometry; // a truncated stroke is a much better failure than an exhausted heap. @(private) MAX_STROKE_POINTS :: 1 << 21 // stroke_cull grows the caller's clip by the farthest a stroke of this pen can // paint from the path it follows: half the widest device-space thickness -- // floored at a hairline, because a hairline pen is one device pixel wide // however thin the file said the line was -- times the miter limit for the // spike a join can add, and two pixels of slack for the flattening tolerance // and the rounding at the edge. Nothing outside that can mark the clip, so a // curve that stays outside it may be replaced by its chord. @(private) stroke_cull :: proc(cull: Maybe(Rect), width, hi, miter: f64, dashed: bool) -> Maybe(Rect) { box, have := cull.? if !have || dashed { return nil } reach := max(width * hi, HAIRLINE_WIDTH) * 0.5 * miter + 2 if !is_finite(reach) { return nil } return Rect{box.x0 - reach, box.y0 - reach, box.x1 + reach, box.y1 + reach} } // stroke_flatten converts the stroke of `path` under `state` into a fillable // outline, appending it to `out`. The result is in DEVICE space, already // flattened, and must be filled with the NONZERO rule. // // Anything already in `out` is kept, so several strokes can be accumulated // into one path -- which is what a clip built from `S n` needs. // // `flatness` is the maximum distance, in device pixels, between the true // outline and the polygon that replaces it. // // `cull`, when given, is the device-space region the outline will be drawn // through. It is passed on to `path_flatten` grown by the farthest the pen can // reach from the path, so a curve that cannot paint into the clip is not // subdivided -- unless the stroke is dashed, where it must be: dashes are laid // out by arc length along the flattened polyline, so shortening an off-screen // excursion would shift the phase of every dash after it, including the ones // on screen. stroke_flatten :: proc(out: ^Flat_Path, path: ^Path, state: ^Stroke_State, ctm: Matrix, flatness := FLATNESS, cull: Maybe(Rect) = nil) -> Error { if out == nil || path == nil { return .Invalid_Argument } if !matrix_is_finite(ctm) { return .Invalid_Argument } if len(path.verbs) == 0 { return .None } st := state != nil ? state^ : STROKE_STATE_DEFAULT // The device-space thickness of a stroke of width w varies with the // direction of travel between w*lo and w*hi, so these two numbers are // exactly what the hairline test and the dash-visibility test need. lo, hi := stroke_scale_extremes(ctm) if hi == 0 { return .None } // A singular CTM -- or one so close to singular that its inverse is // numerical noise -- maps the outline onto a line: zero area, nothing to // paint. Content is deliberately hidden this way, so it is not an error. if lo <= hi * 1e-9 { return .None } inv, invertible := matrix_invert(ctm) if !invertible { return .None } tol_dev := flatness if !is_finite(tol_dev) || tol_dev <= 0 { tol_dev = FLATNESS } width := st.line_width if !is_finite(width) || width < 0 { width = 0 } // The hairline test is against the *widest* thickness the stroke can // reach. An anisotropic CTM that paints one pixel across one axis and // fifty across the other is a real stroke, not a hairline, and promoting // it would replace a wedge of ink with a thread. hairline := width * hi < HAIRLINE_WIDTH // ... but the narrow axis of that same wedge still has to be floored, and // testing only the widest thickness never gets to it. What a stroke of // width w comes out at in device space depends on the direction it runs // in -- it is w times the stretch ACROSS that direction, anywhere between // w*lo and w*hi -- so under `1 0 0 0.02 0 0 cm` a horizontal rule at // `1 w` is painted 0.02 pixels tall: a 1% grey that reads as blank paper, // and the same rule at `0 w` is a clean hairline. Widening the pen must // not make a line disappear. // // The pen is an ellipse whenever the CTM is anisotropic, so the fix is to // floor its short axis at one device pixel and keep its long one: the // thin direction comes out a hairline, the thick direction keeps its true // width, and no direction is ever painted thinner than the thinnest line // the device can draw (8.4.3.2). mupdf and pdfium make this decision with // a single averaged scale factor instead, which draws this rule but thins // the wedge case the paragraph above is about. squashed := !hairline && width * lo < HAIRLINE_WIDTH sq_post, sq_build, sq_ok := Matrix{}, Matrix{}, false if squashed { sq_post, sq_build, sq_ok = stroke_squashed_pen(ctm, width, lo, hi) } s := Stroker{ out = out, line_cap = st.line_cap, line_join = st.line_join, // A miter limit below 1 is geometrically impossible (the ratio is // never less than 1) and files do carry 0; clamping keeps a bevel // from being forced on every join. 8.4.3.5. miter = is_finite(st.miter_limit) ? max(st.miter_limit, 1) : 10, } if hairline { // A hairline is built directly in device space, so it comes out // exactly one pixel wide in every direction rather than one pixel // on average. s.radius = HAIRLINE_WIDTH * 0.5 s.to_build = ctm s.post = MATRIX_IDENTITY s.post_id = true s.tol = tol_dev } else if sq_ok { // Build space is the one the floored pen is a circle in, exactly as // for a hairline; the difference is that `post` stretches it back out // along the CTM's long axis instead of being the identity. s.radius = HAIRLINE_WIDTH * 0.5 s.to_build = sq_build s.post = sq_post s.tol = max(tol_dev / max(width * hi, 1), 1e-9) } else { s.radius = width * 0.5 s.to_build = MATRIX_IDENTITY s.build_id = true s.post = ctm // hi rather than the average expansion: under an anisotropic CTM the // stretched axis is the one that shows the faceting. s.tol = max(tol_dev / hi, 1e-9) } s.pts = make([dynamic]Point, 0, 64) defer stroke_destroy(&s) // Flatten into device space so the tolerance means device pixels, then // pull the polyline back into user space: both the dash pattern and the // pen are user-space quantities. scratch := flat_path_make() defer flat_path_destroy(&scratch) path_flatten(&scratch, path, ctm, tol_dev, stroke_cull(cull, width, hi, s.miter, stroke_has_dash(&st))) if len(scratch.points) == 0 { return .None } device_len := 0.0 for i in 0 ..< len(scratch.subpaths) { pts := flat_path_slice(&scratch, i) for k in 1 ..< len(pts) { device_len += stroke_len(pts[k].x - pts[k - 1].x, pts[k].y - pts[k - 1].y) } if scratch.subpaths[i].closed && len(pts) > 1 { device_len += stroke_len(pts[0].x - pts[len(pts) - 1].x, pts[0].y - pts[len(pts) - 1].y) } } if !is_finite(device_len) || device_len < 0 { device_len = 0 } for &p in scratch.points { p = transform_point(inv, p) } dashed := stroke_has_dash(&st) dash_count := min(st.dash_count, MAX_DASH_ENTRIES) total := 0.0 if dashed { for i in 0 ..< dash_count { total += st.dash[i] } // A pattern whose entire cycle is under one device pixel cannot read // as anything but a solid line, and dashing it would emit one piece // per sub-pixel step: unbounded geometry for no visible difference. if !is_finite(total) || total * hi < 1 { dashed = false } } if dashed { d := Stroke_Dasher{s = &s, count = dash_count, total = total, phase = st.dash_phase} for i in 0 ..< dash_count { d.pattern[i] = st.dash[i] } // No more dashes can be told apart than there are pixels along the // path, so the piece count is capped by the path's device-space // length. Without it a `[1e-6 1e-6] 0 d` pattern on a 10000-pixel line // would generate ten billion rectangles. budget := f64(MAX_DASH_PIECES) if device_len < budget { budget = device_len } d.budget = int(budget) + 16 d.piece = make([dynamic]Point, 0, 64) d.head = make([dynamic]Point, 0, 16) defer stroke_dasher_destroy(&d) for i in 0 ..< len(scratch.subpaths) { stroke_dash_polyline(&d, flat_path_slice(&scratch, i), scratch.subpaths[i].closed) } } else { for i in 0 ..< len(scratch.subpaths) { stroke_subpath(&s, flat_path_slice(&scratch, i), scratch.subpaths[i].closed) } } return .None } // --------------------------------------------------------------------------- // The stroker // --------------------------------------------------------------------------- // Stroker carries everything the outline builders need. "Build space" is the // space the pen is circular in: user space for an ordinary stroke, device // space for a hairline. @(private) Stroker :: struct { out: ^Flat_Path, radius: f64, line_cap: Line_Cap, line_join: Line_Join, miter: f64, // Flattening tolerance expressed in build space. tol: f64, to_build: Matrix, // user space -> build space build_id: bool, post: Matrix, // build space -> device space post_id: bool, // One subpath's points in build space, reused between subpaths. pts: [dynamic]Point, // Index into out.points at which the contour under construction started. start: int, // A non-finite point reached this contour, so the whole contour is // dropped rather than letting a NaN through to the rasteriser. bad: bool, full: bool, } @(private) stroke_destroy :: proc(s: ^Stroker) { delete(s.pts) s.pts = nil } // stroke_scale_extremes returns the singular values of the matrix's linear // part: the smallest and largest factors by which it stretches a direction. @(private) stroke_scale_extremes :: proc "contextless" (m: Matrix) -> (lo, hi: f64) { sum := m.a * m.a + m.b * m.b + m.c * m.c + m.d * m.d det := m.a * m.d - m.b * m.c disc := sum * sum - 4 * det * det if disc < 0 || !is_finite(disc) { disc = 0 } root := math.sqrt(disc) hi2 := (sum + root) * 0.5 lo2 := (sum - root) * 0.5 if lo2 < 0 { lo2 = 0 } if !is_finite(hi2) || hi2 < 0 { return 0, 0 } return math.sqrt(lo2), math.sqrt(hi2) } // stroke_squashed_pen builds the transform pair for a pen that is under one // device pixel across its short axis but not its long one. In build space the // pen is the same circle of radius HAIRLINE_WIDTH/2 a hairline uses, and // `post` maps that circle onto the CTM's own ellipse with its short semi-axis // raised to HAIRLINE_WIDTH/2. `post` composed after `to_build` is the CTM to // the last bit, so the path itself still lands exactly where the CTM puts it; // only the shape of the pen changes. // // ok = false for a CTM whose ellipse cannot be built (an axis that overflows, // or a product that is not invertible), leaving the caller its ordinary path. @(private) stroke_squashed_pen :: proc "contextless" (ctm: Matrix, width, lo, hi: f64) -> (post, to_build: Matrix, ok: bool) { r := width * 0.5 unit := HAIRLINE_WIDTH * 0.5 major := max(r * hi, unit) minor := max(r * lo, unit) // The long axis of the ellipse is the left singular vector of the CTM's // linear part belonging to `hi`. M*Mt is symmetric with the left singular // vectors as its eigenvectors and hi^2, lo^2 as its eigenvalues, and for // [[p q] [q s]] the eigenvector for lambda is (q, lambda-p), or equally // (lambda-s, q); the longer of the two is the numerically sound one. // // M is divided through by `hi` first, which leaves the eigenvectors where // they were and makes the largest eigenvalue exactly 1, so that a CTM // scaled by 1e200 cannot overflow the squares. ma, mb := ctm.a / hi, ctm.b / hi mc, md := ctm.c / hi, ctm.d / hi p := ma * ma + mc * mc s := mb * mb + md * md q := ma * mb + mc * md lam := 1.0 v1 := Point{q, lam - p} v2 := Point{lam - s, q} u := stroke_len(v1.x, v1.y) >= stroke_len(v2.x, v2.y) ? v1 : v2 l := stroke_len(u.x, u.y) // Both candidates vanish only when M*Mt is a multiple of the identity, in // which case every direction is a singular vector and the axes may as // well be the coordinate ones. if l <= 0 { u, l = Point{1, 0}, 1 } u = Point{u.x / l, u.y / l} g1 := major / unit g2 := minor / unit post = Matrix{a = u.x * g1, b = u.y * g1, c = -u.y * g2, d = u.x * g2, e = 0, f = 0} inv := matrix_invert(post) or_return to_build = matrix_mul(ctm, inv) if !matrix_is_finite(post) || !matrix_is_finite(to_build) { return MATRIX_IDENTITY, MATRIX_IDENTITY, false } return post, to_build, true } @(private) stroke_len :: proc "contextless" (dx, dy: f64) -> f64 { l := math.sqrt(dx * dx + dy * dy) return is_finite(l) ? l : 0 } // --------------------------------------------------------------------------- // Contour assembly // --------------------------------------------------------------------------- @(private) stroke_contour_begin :: proc(s: ^Stroker) { s.start = len(s.out.points) s.bad = false } @(private) stroke_emit :: proc(s: ^Stroker, p: Point) { if s.full { return } if len(s.out.points) >= MAX_STROKE_POINTS { s.full = true s.bad = true return } if !is_finite(p.x) || !is_finite(p.y) { s.bad = true return } append(&s.out.points, p) } @(private) stroke_contour_end :: proc(s: ^Stroker) { count := len(s.out.points) - s.start // Fewer than three points bounds no area; emitting it would only cost the // rasteriser a pass over edges that cancel. if s.bad || count < 3 { resize(&s.out.points, s.start) return } if !s.post_id { for k in s.start ..< len(s.out.points) { q := transform_point(s.post, s.out.points[k]) // stroke_emit checked these points in BUILD space, and the trip // to device space can overflow one that was perfectly finite // there: an outline point of order 5e159 under a 1e150 CTM is // +Inf, and a CTM that rotates or skews adds +Inf to -Inf and // gets NaN. Neither may reach a Flat_Path -- ARCHITECTURE.md // rule 5, and this is below path.odin's construction-time filter // -- and the rasteriser's clamping is not a defence, because it // is not the only thing that reads one. if !is_finite(q.x) || !is_finite(q.y) { resize(&s.out.points, s.start) return } s.out.points[k] = q } } append(&s.out.subpaths, Flat_Subpath{start = s.start, count = count, closed = true}) } // --------------------------------------------------------------------------- // One subpath // --------------------------------------------------------------------------- // stroke_subpath takes a polyline in USER space, moves it into build space, // removes the zero-length segments that would leave a normal undefined, and // emits its outline. @(private) stroke_subpath :: proc(s: ^Stroker, user_pts: []Point, closed: bool) { if s.full || len(user_pts) == 0 { return } clear(&s.pts) for up in user_pts { q := s.build_id ? up : transform_point(s.to_build, up) if !is_finite(q.x) || !is_finite(q.y) { continue } if len(s.pts) > 0 { prev := s.pts[len(s.pts) - 1] if stroke_len(q.x - prev.x, q.y - prev.y) <= 0 { continue } } append(&s.pts, q) } if closed { // A flattened closed subpath does not repeat its first point, but a // path written "m 0 0 l 10 0 l 0 0 h" ends where it started and the // repeat would be a zero-length closing segment. for len(s.pts) > 1 { back := s.pts[len(s.pts) - 1] if stroke_len(back.x - s.pts[0].x, back.y - s.pts[0].y) > 0 { break } pop(&s.pts) } } if len(s.pts) == 0 { return } if len(s.pts) == 1 { stroke_dot(s, s.pts[0]) return } pts := s.pts[:] if closed { // A closed subpath joins its end to its start instead of capping, and // it needs two rings: the outer one in the direction of travel and the // inner one against it, so the nonzero rule leaves the middle empty. stroke_contour_begin(s) stroke_side(s, pts, true, false) stroke_contour_end(s) stroke_contour_begin(s) stroke_side(s, pts, true, true) stroke_contour_end(s) return } // An open subpath is one loop: up one side, round the far cap, back down // the other side (which is the same walk over the reversed points), round // the near cap. stroke_contour_begin(s) _, end_normal, ok := stroke_side(s, pts, false, false) if ok { stroke_cap(s, pts[len(pts) - 1], end_normal) _, start_normal, _ := stroke_side(s, pts, false, true) stroke_cap(s, pts[0], start_normal) } stroke_contour_end(s) } // stroke_side walks one side of the polyline, emitting the offset segments and // the join geometry between them. The offset is taken along the left normal of // the direction of travel, so walking the points backwards traces the other // side. `first` and `last` are the normals of the first and last segments, // which is what the caller needs to place the caps. @(private) stroke_side :: proc(s: ^Stroker, pts: []Point, closed, reverse: bool) -> (first, last: Point, ok: bool) { n := len(pts) if n < 2 { return } r := s.radius seg_count := closed ? n : n - 1 for i in 0 ..< seg_count { j := (i + 1) % n a := pts[reverse ? n - 1 - i : i] b := pts[reverse ? n - 1 - j : j] dx := b.x - a.x dy := b.y - a.y l := stroke_len(dx, dy) if l <= 0 { continue } nrm := Point{-dy / l, dx / l} if ok { stroke_join(s, a, last, nrm) } else { first = nrm ok = true } stroke_emit(s, Point{a.x + r * nrm.x, a.y + r * nrm.y}) stroke_emit(s, Point{b.x + r * nrm.x, b.y + r * nrm.y}) last = nrm } if closed && ok { stroke_join(s, pts[reverse ? n - 1 : 0], last, first) } return } // stroke_join emits whatever lies between the offset point the previous // segment ended on (v + r*n0) and the one the next segment starts from // (v + r*n1). Both of those points are emitted by the caller, so a bevel is // literally nothing at all. @(private) stroke_join :: proc(s: ^Stroker, v, n0, n1: Point) { cross := n0.x * n1.y - n0.y * n1.x dot := n0.x * n1.x + n0.y * n1.y r := s.radius // Every vertex of a flattened curve is a join between nearly collinear // segments. When the two offset points are within the flattening // tolerance of each other, everything a join could add between them -- // arc, miter point or the vertex itself -- lies within that tolerance of // the straight edge already there. Dropping those keeps a stroked curve // to roughly twice the size of the curve instead of three times. cx := r * (n1.x - n0.x) cy := r * (n1.y - n0.y) if cx * cx + cy * cy <= s.tol * s.tol { return } if cross > 0 { // The inside of the turn: the two offset segments already overlap, so // there is no gap to fill. Taking the contour through the vertex keeps // it a single loop and the nonzero rule paints the overlap once. stroke_emit(s, v) return } if cross == 0 && dot > 0 { return } // collinear: the offsets already meet switch s.line_join { case .Bevel: // Nothing. The straight edge between the two offset points is the // bevel. case .Round: stroke_arc(s, v, n0, n1) case .Miter: denom := 1 + dot if denom > 1e-12 { // The miter point is where the two offset lines meet: // (q-v).n0 == (q-v).n1 == r has the solution q = v + r(n0+n1)/(1+n0.n1). k := r / denom q := Point{v.x + k * (n0.x + n1.x), v.y + k * (n0.y + n1.y)} // Miter length over line width, which is the ratio /MiterLimit // bounds (8.4.3.5): |q-v|/r == sqrt(2/(1+cos)). if math.sqrt(2 / denom) <= s.miter { stroke_emit(s, q) return } } // Over the miter limit, or a 180-degree reversal whose miter runs off // to infinity: the format says to substitute a bevel, which is what // emitting nothing gives. } } // stroke_cap emits whatever lies between v + r*n and v - r*n at the end of an // open subpath. `n` is the left normal of the last direction of travel, so the // direction of travel itself is (n.y, -n.x). @(private) stroke_cap :: proc(s: ^Stroker, v, n: Point) { r := s.radius switch s.line_cap { case .Butt: // Nothing: the contour closes straight across the end. case .Square: dx := n.y dy := -n.x stroke_emit(s, Point{v.x + r * (n.x + dx), v.y + r * (n.y + dy)}) stroke_emit(s, Point{v.x + r * (dx - n.x), v.y + r * (dy - n.y)}) case .Round: stroke_arc(s, v, n, Point{-n.x, -n.y}) } } // stroke_arc emits the interior points of the arc that sweeps clockwise from // n0 to n1 around `centre`. Clockwise is the direction that stays on the outer // side of a join and outside the end of a cap, which is what keeps every // contour wound the same way. @(private) stroke_arc :: proc(s: ^Stroker, centre, n0, n1: Point) { r := s.radius if r <= 0 { return } a0 := math.atan2(n0.y, n0.x) a1 := math.atan2(n1.y, n1.x) sweep := a0 - a1 if sweep < 0 { sweep += math.TAU } if !(sweep > 0) { return } count := stroke_arc_steps(sweep, r, s.tol, MAX_ARC_SEGMENTS) for k in 1 ..< count { ang := a0 - sweep * f64(k) / f64(count) stroke_emit(s, Point{centre.x + r * math.cos(ang), centre.y + r * math.sin(ang)}) } } // stroke_arc_steps is how many chords an arc of `sweep` radians on a circle of // radius r needs to stay within `tol` of the true circle. The sagitta of a // chord subtending t radians is r*(1-cos(t/2)), which is r*t^2/8 for small t, // so t = sqrt(8*tol/r) is the largest step that keeps inside the tolerance. @(private) stroke_arc_steps :: proc "contextless" (sweep, r, tol: f64, limit: int) -> int { step := math.sqrt(8 * tol / r) if !(step > 0) || !is_finite(step) { return limit } want := sweep / step if !is_finite(want) || want >= f64(limit) { return limit } n := int(math.ceil(want)) return max(n, 1) } // stroke_dot paints a degenerate subpath: a single point, or a closed subpath // with no length. Strictly 8.4.3.3 paints one only under round caps, but every // viewer in practice also paints the projecting square, and a dotted line // written `[0 3] 0 d` with square caps is a real idiom, so both are honoured. @(private) stroke_dot :: proc(s: ^Stroker, v: Point) { r := s.radius if r <= 0 { return } switch s.line_cap { case .Butt: // A degenerate subpath under butt caps paints nothing at all. case .Round: count := stroke_arc_steps(math.TAU, r, s.tol, MAX_CIRCLE_SEGMENTS) count = max(count, 4) stroke_contour_begin(s) // Negative angles: clockwise, matching every other contour. for k in 0 ..< count { ang := -math.TAU * f64(k) / f64(count) stroke_emit(s, Point{v.x + r * math.cos(ang), v.y + r * math.sin(ang)}) } stroke_contour_end(s) case .Square: // The direction of travel is undefined here, so the square is // axis-aligned in build space. stroke_contour_begin(s) stroke_emit(s, Point{v.x - r, v.y - r}) stroke_emit(s, Point{v.x - r, v.y + r}) stroke_emit(s, Point{v.x + r, v.y + r}) stroke_emit(s, Point{v.x + r, v.y - r}) stroke_contour_end(s) } } // --------------------------------------------------------------------------- // Dashing (ISO 32000-2, 8.4.3.6) // // The pattern is measured along the path in user space, starting `phase` units // into it, and the line cap is applied to both ends of every dash. // --------------------------------------------------------------------------- @(private) Stroke_Dasher :: struct { s: ^Stroker, pattern: [MAX_DASH_ENTRIES]f64, count: int, total: f64, phase: f64, idx: int, on: bool, // Length left in the current pattern entry. left: f64, // The dash being accumulated, in user space. piece: [dynamic]Point, // The opening dash of a closed subpath, held back until the walk comes // back round to the start; see stroke_dash_polyline. head: [dynamic]Point, hold: bool, budget: int, } @(private) stroke_dasher_destroy :: proc(d: ^Stroke_Dasher) { delete(d.piece) delete(d.head) d.piece = nil d.head = nil } // stroke_dash_reset restarts the pattern at the beginning of a subpath and // winds it forward by the phase. Each subpath restarts: the phase is measured // from the start of the subpath, not from the start of the path. @(private) stroke_dash_reset :: proc(d: ^Stroke_Dasher) { clear(&d.piece) clear(&d.head) d.hold = false d.idx = 0 d.on = true d.left = d.pattern[0] // An odd-length pattern repeats only after twice its sum, because the // second pass through it swaps on for off. cycle := d.count % 2 == 0 ? d.total : d.total * 2 ph := d.phase if !is_finite(ph) || ph < 0 { ph = 0 } if cycle > 0 && ph >= cycle { ph -= math.floor(ph / cycle) * cycle } if !is_finite(ph) || ph < 0 { ph = 0 } // Two passes over the array is all a phase inside one cycle can need; the // fuel is there because a pattern of zeros would otherwise spin. fuel := 2 * d.count + 4 for ph > 0 && fuel > 0 { if ph < d.left { d.left -= ph break } ph -= d.left d.on = !d.on d.idx = (d.idx + 1) % d.count d.left = d.pattern[d.idx] fuel -= 1 } } @(private) stroke_dash_flush :: proc(d: ^Stroke_Dasher) { if len(d.piece) == 0 { return } if d.hold { // The opening dash of a closed subpath is not stroked where it ends; // stroke_dash_polyline decides what it belongs to once the walk gets // back to the start. clear(&d.head) append(&d.head, ..d.piece[:]) d.hold = false clear(&d.piece) return } stroke_subpath(d.s, d.piece[:], false) clear(&d.piece) d.budget -= 1 } @(private) stroke_dash_polyline :: proc(d: ^Stroke_Dasher, pts: []Point, closed: bool) { n := len(pts) if n == 0 { return } stroke_dash_reset(d) if n == 1 { stroke_subpath(d.s, pts, false) return } if d.on { append(&d.piece, pts[0]) } // A dash that is still on when the walk returns to the start of a closed // subpath is ONE dash crossing that vertex, not two dashes that end // there: 8.4.3.6 has dashes "wrap around curves and corners just as solid // stroked lines do", and only the ends of a dash are capped. So the // opening piece is held back and whatever comes back round is joined onto // the front of it, which leaves the seam vertex in the middle of a single // piece and gives it the join every other vertex it crosses gets. Without // this a dashed rectangle loses one of its four corners. d.hold = closed && d.on seg_count := closed ? n : n - 1 for i in 0 ..< seg_count { a := pts[i] b := pts[(i + 1) % n] dx := b.x - a.x dy := b.y - a.y seg := stroke_len(dx, dy) if seg <= 0 { continue } if d.budget <= 0 { d.on = true d.left = DASH_UNBOUNDED } pos := 0.0 for d.left < seg - pos { pos += d.left u := pos / seg p := Point{a.x + dx * u, a.y + dy * u} if d.on { append(&d.piece, p) stroke_dash_flush(d) } else { clear(&d.piece) append(&d.piece, p) } d.on = !d.on d.idx = (d.idx + 1) % d.count d.left = d.pattern[d.idx] if d.budget <= 0 { // Out of budget. Draw the rest of the path solid rather than // stopping: a line that loses its dashes reads as a missing // pattern, a line that stops halfway reads as a broken // renderer. d.on = true d.left = DASH_UNBOUNDED if len(d.piece) == 0 { append(&d.piece, p) } break } } d.left -= seg - pos if d.on { append(&d.piece, b) } } if d.hold { // No gap ever opened, so the whole closed subpath is one dash and is // stroked exactly as the undashed one would be: two rings, a join at // every vertex, and no cap anywhere. d.hold = false if len(d.piece) > 0 { stroke_subpath(d.s, d.piece[:], true) clear(&d.piece) d.budget -= 1 } return } if len(d.head) > 0 { // The held piece begins at the seam vertex, which the piece that came // back round ends at, so appending it whole leaves one repeated point // that stroke_subpath drops. if !d.on || len(d.piece) == 0 { // The pattern went off before the walk got back: the two pieces // really are separate dashes, and each keeps its own caps. stroke_dash_flush(d) } append(&d.piece, ..d.head[:]) clear(&d.head) } stroke_dash_flush(d) }