Skip to content

Commit 7117477

Browse files
committed
fix(clipper): Improved orthorect clipping
Which should be faster!
1 parent 5cec42b commit 7117477

File tree

1 file changed

+53
-135
lines changed

1 file changed

+53
-135
lines changed
Lines changed: 53 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#import "/src/cetz.typ"
2+
13
/// Clip line-strip in rect
24
///
35
/// - points (array): Array of vectors representing a line-strip
@@ -11,158 +13,74 @@
1113
let (min-y, max-y) = (calc.min(low.at(1), high.at(1)),
1214
calc.max(low.at(1), high.at(1)))
1315

14-
let in-rect(pt) = {
15-
return (pt.at(0) >= min-x and pt.at(0) <= max-x and
16-
pt.at(1) >= min-y and pt.at(1) <= max-y)
17-
}
18-
19-
let interpolated-end(a, b) = {
20-
if in-rect(a) and in-rect(b) {
21-
return b
22-
}
23-
24-
let (x1, y1, ..) = a
25-
let (x2, y2, ..) = b
26-
27-
if x2 - x1 == 0 {
28-
return (x2, calc.min(max-y, calc.max(y2, min-y)))
29-
}
30-
31-
if y2 - y1 == 0 {
32-
return (calc.min(max-x, calc.max(x2, min-x)), y2)
33-
}
34-
35-
let m = (y2 - y1) / (x2 - x1)
36-
let n = y2 - m * x2
37-
38-
let x = x2
39-
let y = y2
40-
41-
y = calc.min(max-y, calc.max(y, min-y))
42-
x = (y - n) / m
43-
44-
x = calc.min(max-x, calc.max(x, min-x))
45-
y = m * x + n
46-
47-
return (x, y)
16+
let in-rect((x, y)) = {
17+
return (x >= min-x and x <= max-x and
18+
y >= min-y and y <= max-y)
4819
}
4920

50-
// Append path to paths and return paths
51-
//
52-
// If path starts or ends with a vector of another part, merge those
53-
// paths instead appending path as a new path.
54-
let append-path(paths, path) = {
55-
if path.len() <= 1 {
56-
return paths
57-
}
58-
59-
let cmp(a, b) = {
60-
return a.map(calc.round.with(digits: 8)) == b.map(calc.round.with(digits: 8))
61-
}
21+
let edges = (
22+
((min-x, min-y), (min-x, max-y)),
23+
((max-x, min-y), (max-x, max-y)),
24+
((min-x, min-y), (max-x, min-y)),
25+
((min-x, max-y), (max-x, max-y)),
26+
)
6227

63-
let added = false
64-
for i in range(0, paths.len()) {
65-
let p = paths.at(i)
66-
if cmp(p.first(), path.last()) {
67-
paths.at(i) = path + p
68-
added = true
69-
} else if cmp(p.first(), path.first()) {
70-
paths.at(i) = path.rev() + p
71-
added = true
72-
} else if cmp(p.last(), path.first()) {
73-
paths.at(i) = p + path
74-
added = true
75-
} else if cmp(p.last(), path.last()) {
76-
paths.at(i) = p + path.rev()
77-
added = true
28+
let interpolated-end(a, b) = {
29+
for (edge-a, edge-b) in edges {
30+
let pt = cetz.intersection.line-line(a, b, edge-a, edge-b)
31+
if pt != none {
32+
return pt
7833
}
79-
if added { break }
8034
}
81-
82-
if not added {
83-
paths.push(path)
84-
}
85-
return paths
8635
}
8736

88-
let clamped-pt(pt) = {
89-
return (calc.max(min-x, calc.min(pt.at(0), max-x)),
90-
calc.max(min-y, calc.min(pt.at(1), max-y)))
91-
}
9237

93-
let paths = ()
38+
// Find lines crossing the rect bounds
39+
// by storing all crossings as tuples (<index>, <goes-inside>, <point-on-border>)
40+
let crossings = ()
9441

95-
let path = ()
96-
let prev = points.at(0)
97-
let was-inside = in-rect(prev)
42+
// Push a pseudo entry for the last point, if it is insides the bounds.
43+
let was-inside = in-rect(points.at(0))
9844
if was-inside {
99-
path.push(prev)
100-
} else if fill {
101-
path.push(clamped-pt(prev))
45+
crossings.push((0, true, points.first()))
10246
}
10347

48+
// Find crossings and compute interseciton points.
10449
for i in range(1, points.len()) {
105-
let prev = points.at(i - 1)
106-
let pt = points.at(i)
107-
108-
let is-inside = in-rect(pt)
109-
110-
let (x1, y1) = prev
111-
let (x2, y2) = pt
112-
113-
// Ignore lines if both ends are outsides the x-window and on the
114-
// same side.
115-
if (x1 < min-x and x2 < min-x) or (x1 > max-x and x2 > max-x) {
116-
if fill {
117-
let clamped = clamped-pt(pt)
118-
if path.last() != clamped {
119-
path.push(clamped)
120-
}
121-
}
122-
was-inside = false
123-
continue
50+
let current-inside = in-rect(points.at(i))
51+
if current-inside != was-inside {
52+
crossings.push((
53+
i,
54+
current-inside,
55+
interpolated-end(points.at(i - 1), points.at(i))))
56+
was-inside = current-inside
12457
}
125-
126-
if is-inside {
127-
if was-inside {
128-
path.push(pt)
129-
} else {
130-
path.push(interpolated-end(pt, prev))
131-
path.push(pt)
132-
}
133-
} else {
134-
if was-inside {
135-
path.push(interpolated-end(prev, pt))
136-
} else {
137-
let (a, b) = (interpolated-end(pt, prev),
138-
interpolated-end(prev, pt))
139-
if in-rect(a) and in-rect(b) {
140-
path.push(a)
141-
path.push(b)
142-
} else if fill {
143-
let clamped = clamped-pt(pt)
144-
if path.last() != clamped {
145-
path.push(clamped)
146-
}
147-
}
148-
}
149-
150-
if path.len() > 0 and not fill {
151-
paths = append-path(paths, path)
152-
path = ()
153-
}
154-
}
155-
156-
was-inside = is-inside
15758
}
15859

159-
// Append clamped last point if filling
160-
if fill and not in-rect(points.last()) {
161-
path.push(clamped-pt(points.last()))
60+
// Push a pseudo entry for the last point, if it is insides the bounds.
61+
if in-rect(points.last()) and crossings.last().at(1) {
62+
crossings.push((points.len() - 1, false, points.last()))
16263
}
16364

164-
if path.len() > 1 {
165-
paths = append-path(paths, path)
65+
// Generate paths
66+
let paths = ()
67+
for i in range(1, crossings.len()) {
68+
let (a-index, a-dir, a-pt) = crossings.at(i - 1)
69+
let (b-index, b-dir, b-pt) = crossings.at(i)
70+
71+
if a-dir {
72+
let path = points.slice(a-index, b-index)
73+
path.insert(0, a-pt)
74+
path.push(b-pt)
75+
76+
// Insert the last end point to connect
77+
// to a filled area.
78+
if fill and paths.len() > 0 {
79+
path.insert(0, paths.last().last())
80+
}
81+
82+
paths.push(path)
83+
}
16684
}
16785

16886
return paths
@@ -181,4 +99,4 @@
18199
/// - low (vector): Lower clip-window coordinate
182100
/// - high (vector): Upper clip-window coordinate
183101
/// -> array List of fill paths
184-
#let compute-fill-paths = clipped-paths-rect.with(fill: true)
102+
#let compute-fill-paths = clipped-paths-rect.with(fill: true)

0 commit comments

Comments
 (0)