-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathlib.rs
More file actions
332 lines (286 loc) · 11.7 KB
/
lib.rs
File metadata and controls
332 lines (286 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
use core_types::table::{Table, TableRow, TableRowRef};
use core_types::{Color, Ctx};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphic_types::vector_types::vector::style::Fill;
use graphic_types::{Graphic, Vector};
use linesweeper::topology::Topology;
use linesweeper::{BinaryOp, FillRule, binary_op};
use smallvec::SmallVec;
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
pub use vector_types::vector::misc::BooleanOperation;
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
// TODO: since before we used a Vec of single-row tables and now we use a single table
// TODO: with multiple rows while still assuming a single row for the boolean operations.
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category(""))]
async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clone>(
_: impl Ctx,
/// The table of vector paths to perform the boolean operation on. Nested tables are automatically flattened.
#[implementations(Table<Graphic>, Table<Vector>)]
content: I,
/// Which boolean operation to perform on the paths.
///
/// Union combines all paths while cutting out overlapping areas (even the interiors of a single path).
/// Subtraction cuts overlapping areas out from the last (Subtract Front) or first (Subtract Back) path.
/// Intersection cuts away all but the overlapping areas shared by every path.
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
operation: BooleanOperation,
) -> Table<Vector> {
let content = content.into_graphic_table();
// The first index is the bottom of the stack
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&content).iter(), operation);
// Replace the transformation matrix with a mutation of the vector points themselves
if let Some(result_vector) = result_vector_table.iter_mut().next() {
let transform = *result_vector.transform;
*result_vector.transform = DAffine2::IDENTITY;
Vector::transform(result_vector.element, transform);
result_vector.element.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.element.upstream_data = Some(content.clone());
// Clean up the boolean operation result by merging duplicated points
result_vector.element.merge_by_distance_spatial(*result_vector.transform, 0.0001);
}
result_vector_table
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct WindingNumber {
elems: SmallVec<[i16; 8]>,
}
impl linesweeper::topology::WindingNumber for WindingNumber {
type Tag = (usize, usize);
fn single((tag, out_of): (usize, usize), positive: bool) -> Self {
let mut elems = SmallVec::with_capacity(out_of);
elems.resize(out_of, 0);
elems[tag] = if positive { 1 } else { -1 };
Self { elems }
}
}
impl std::ops::AddAssign for WindingNumber {
fn add_assign(&mut self, rhs: Self) {
if rhs.elems.is_empty() {
return;
}
if self.elems.is_empty() {
self.elems = rhs.elems;
} else {
for (me, them) in self.elems.iter_mut().zip(&rhs.elems) {
*me += *them;
}
}
}
}
impl std::ops::Add for WindingNumber {
type Output = WindingNumber;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl WindingNumber {
fn is_inside(&self, op: BooleanOperation) -> bool {
let is_in = |w: &i16| *w != 0;
let is_out = |w: &i16| *w == 0;
match op {
BooleanOperation::Union => self.elems.iter().any(is_in),
BooleanOperation::SubtractFront => self.elems.first().is_some_and(is_in) && self.elems.iter().skip(1).all(is_out),
BooleanOperation::SubtractBack => self.elems.last().is_some_and(is_in) && self.elems.iter().rev().skip(1).all(is_out),
BooleanOperation::Intersect => !self.elems.is_empty() && self.elems.iter().all(is_in),
BooleanOperation::Difference => self.elems.iter().any(is_in) && !self.elems.iter().all(is_in),
}
}
}
fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone, boolean_operation: BooleanOperation) -> Table<Vector> {
const EPSILON: f64 = 1e-5;
let mut table = Table::new();
let mut paths = Vec::new();
let mut row = TableRow::<Vector>::default();
let copy_from = if matches!(boolean_operation, BooleanOperation::SubtractFront) {
vector.clone().next()
} else {
vector.clone().next_back()
};
if let Some(copy_from) = copy_from {
row.alpha_blending = *copy_from.alpha_blending;
row.source_node_id = *copy_from.source_node_id;
row.element.style = copy_from.element.style.clone();
row.element.upstream_data = copy_from.element.upstream_data.clone();
}
for element in vector {
paths.push(to_bez_path(element.element, *element.transform));
}
let top = match Topology::<WindingNumber>::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) {
Ok(top) => top,
Err(e) => {
log::error!("Boolean operation failed while building topology: {e}");
table.push(row);
return table;
}
};
let contours = top.contours(|winding| winding.is_inside(boolean_operation));
for subpath in from_bez_paths(contours.contours().map(|c| &c.path)) {
row.element.append_subpath(subpath, false);
}
row.element.detect_colinear_manipulators();
table.push(row);
table
}
fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
graphic_table
.iter()
.flat_map(|element| {
match element.element.clone() {
Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the vector table
vector
.into_iter()
.map(|mut sub_vector| {
sub_vector.transform = *element.transform * sub_vector.transform;
sub_vector
})
.collect::<Vec<_>>()
}
Graphic::RasterCPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::Graphic(mut graphic) => {
// Apply the parent graphic's transform to each element of inner table
for sub_element in graphic.iter_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner table into the output vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&graphic).iter(), BooleanOperation::Union);
unioned.into_iter().collect::<Vec<_>>()
}
Graphic::Color(color) => color
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Solid(row.element));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
Graphic::Gradient(gradient) => gradient
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient {
stops: row.element,
..Default::default()
}));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
}
})
.collect()
}
// This quantization should potentially be removed since it's not conceptually necessary,
// but without it, the oak leaf in the Changing Seasons demo artwork is funky because
// quantization is needed for the top and bottom points to line up vertically.
fn quantize_segment(seg: PathSeg) -> PathSeg {
const QUANTIZE_EPS: f64 = 1e-8;
fn q(p: Point) -> Point {
Point::new((p.x / QUANTIZE_EPS).round() * QUANTIZE_EPS, (p.y / QUANTIZE_EPS).round() * QUANTIZE_EPS)
}
match seg {
PathSeg::Line(s) => PathSeg::Line(Line::new(q(s.p0), q(s.p1))),
PathSeg::Quad(s) => PathSeg::Quad(QuadBez::new(q(s.p0), q(s.p1), q(s.p2))),
PathSeg::Cubic(s) => PathSeg::Cubic(CubicBez::new(q(s.p0), q(s.p1), q(s.p2), q(s.p3))),
}
}
fn to_bez_path(vector: &Vector, transform: DAffine2) -> BezPath {
let mut path = BezPath::new();
for subpath in vector.stroke_bezier_paths() {
push_subpath(&mut path, &subpath, transform);
}
path
}
fn push_subpath(path: &mut BezPath, subpath: &Subpath<PointId>, transform: DAffine2) {
let transform = Affine::new(transform.to_cols_array());
let mut first = true;
for seg in subpath.iter_closed() {
let quantized = quantize_segment(transform * seg);
if first {
first = false;
path.move_to(quantized.start());
}
path.push(quantized.as_path_el());
}
path.close_path();
}
fn from_bez_paths<'a>(paths: impl Iterator<Item = &'a BezPath>) -> Vec<Subpath<PointId>> {
let mut all_subpaths = Vec::new();
for path in paths {
let cubics: Vec<CubicBez> = path.segments().map(|segment| segment.to_cubic()).collect();
let mut manipulators_list = Vec::new();
let mut current_start = None;
for (index, cubic) in cubics.iter().enumerate() {
let d = |p: Point| DVec2::new(p.x, p.y);
let [start, handle1, handle2, end] = [d(cubic.p0), d(cubic.p1), d(cubic.p2), d(cubic.p3)];
if current_start.is_none() {
// Use the correct in-handle (None) and out-handle for the start point
manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1)));
} else {
// Update the out-handle of the previous point
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(handle1);
}
}
// Add the end point with the correct in-handle and out-handle (None)
manipulators_list.push(ManipulatorGroup::new(end, Some(handle2), None));
current_start = Some(end);
// Check if this is the last segment
if index == cubics.len() - 1 {
all_subpaths.push(Subpath::new(manipulators_list, true));
manipulators_list = Vec::new(); // Reset manipulators for the next path
}
}
}
all_subpaths
}
pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec<BezPath> {
match binary_op(a, b, FillRule::NonZero, BinaryOp::Intersection) {
Ok(contours) => contours.contours().map(|c| c.path.clone()).collect(),
Err(e) => {
log::error!("Boolean Operation failed (a: {} segments, b: {} segments): {e}", a.segments().count(), b.segments().count());
Vec::new()
}
}
}