1
+ # import " /src/cetz.typ"
2
+
1
3
// / Clip line-strip in rect
2
4
// /
3
5
// / - points (array): Array of vectors representing a line-strip
4
6
// / - low (vector): Lower clip-window coordinate
5
7
// / - high (vector): Upper clip-window coordinate
8
+ // / - fill (bool): Return fillable shapes
9
+ // / - generate-edge-points (bool): Generate interpolated points on clipped edges
6
10
// / -> array List of line-strips representing the paths insides the clip-window
7
- # let clipped-paths-rect (points , ctx , fill : false ) = {
11
+ # let clipped-paths-rect (points , ctx , fill : false , generate-edge-points : false ) = {
8
12
let (low , high ) = ctx . clip
9
13
let (min-x , max-x ) = (calc . min (low . at (0 ), high . at (0 )),
10
14
calc . max (low . at (0 ), high . at (0 )))
11
15
let (min-y , max-y ) = (calc . min (low . at (1 ), high . at (1 )),
12
16
calc . max (low . at (1 ), high . at (1 )))
13
17
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 )
18
+ let in-rect ((x , y )) = {
19
+ return (x >= min-x and x <= max-x and
20
+ y >= min-y and y <= max-y )
48
21
}
49
22
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
- }
23
+ let edges = (
24
+ ((min-x , min-y ), (min-x , max-y )),
25
+ ((max-x , min-y ), (max-x , max-y )),
26
+ ((min-x , min-y ), (max-x , min-y )),
27
+ ((min-x , max-y ), (max-x , max-y )),
28
+ )
62
29
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
30
+ let interpolated-end (a , b ) = {
31
+ for (edge-a , edge-b ) in edges {
32
+ let pt = cetz . intersection . line-line (a , b , edge-a , edge-b )
33
+ if pt != none {
34
+ return pt
78
35
}
79
- if added { break }
80
36
}
81
-
82
- if not added {
83
- paths . push (path )
84
- }
85
- return paths
86
37
}
87
38
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
- }
92
39
93
- let paths = ()
40
+ // Find lines crossing the rect bounds
41
+ // by storing all crossings as tuples (<index>, <goes-inside>, <point-on-border>)
42
+ let crossings = ()
94
43
95
- let path = ()
96
- let prev = points . at (0 )
97
- let was-inside = in-rect (prev )
44
+ // Push a pseudo entry for the last point, if it is insides the bounds.
45
+ let was-inside = in-rect (points . at (0 ))
98
46
if was-inside {
99
- path . push (prev )
100
- } else if fill {
101
- path . push (clamped-pt (prev ))
47
+ crossings . push ((0 , true , points . first ()))
102
48
}
103
49
50
+ // Find crossings and compute interseciton points.
104
51
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 )
52
+ let current-inside = in-rect (points . at (i ))
53
+ if current-inside != was-inside {
54
+ crossings . push ((
55
+ i ,
56
+ current-inside ,
57
+ interpolated-end (points . at (i - 1 ), points . at (i ))))
58
+ was-inside = current-inside
59
+ }
60
+ }
109
61
110
- let (x1 , y1 ) = prev
111
- let (x2 , y2 ) = pt
62
+ // Push a pseudo entry for the last point, if it is insides the bounds.
63
+ if in-rect (points . last ()) and crossings . last (). at (1 ) {
64
+ crossings . push ((points . len () - 1 , false , points . last ()))
65
+ }
112
66
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 )
67
+ // Generate paths
68
+ let paths = ()
69
+ for i in range (1 , crossings . len ()) {
70
+ let (a-index , a-dir , a-pt ) = crossings . at (i - 1 )
71
+ let (b-index , b-dir , b-pt ) = crossings . at (i )
72
+
73
+ if a-dir {
74
+ let path = ()
75
+
76
+ // If we must generate edge points, take the previous crossing
77
+ // as source point and interpolate between that and the current one.
78
+ if generate-edge-points and i > 2 {
79
+ let (c-index , c-dir , c-pt ) = crossings . at (i - 2 )
80
+
81
+ let n = a-index - c-index
82
+ if n > 1 {
83
+ path += range (0 , n ). map (t => {
84
+ cetz . vector . lerp (c-pt , a-pt , t / (n - 1 ))
85
+ })
120
86
}
121
87
}
122
- was-inside = false
123
- continue
124
- }
125
88
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
- }
89
+ // Append the path insides the bounds
90
+ path . push (a-pt )
91
+ path += points . slice (a-index , b-index )
92
+ path . push (b-pt )
149
93
150
- if path . len () > 0 and not fill {
151
- paths = append-path (paths , path )
152
- path = ()
94
+ // Insert the last end point to connect
95
+ // to a filled area.
96
+ if fill and paths . len () > 0 {
97
+ path . insert (0 , paths . last (). last ())
153
98
}
154
- }
155
-
156
- was-inside = is-inside
157
- }
158
99
159
- // Append clamped last point if filling
160
- if fill and not in-rect (points . last ()) {
161
- path . push (clamped-pt (points . last ()))
162
- }
163
-
164
- if path . len () > 1 {
165
- paths = append-path (paths , path )
100
+ paths . push (path )
101
+ }
166
102
}
167
103
168
104
return paths
181
117
// / - low (vector): Lower clip-window coordinate
182
118
// / - high (vector): Upper clip-window coordinate
183
119
// / -> array List of fill paths
184
- # let compute-fill-paths = clipped-paths-rect . with (fill : true )
120
+ # let compute-fill-paths = clipped-paths-rect . with (fill : true )
0 commit comments