Skip to content

Commit 4afadeb

Browse files
committed
feat(script): add OutlineExpr::Bool
1 parent 6c47522 commit 4afadeb

6 files changed

Lines changed: 156 additions & 38 deletions

File tree

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/monoxide-curves/src/cube.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
mod exchange;
55

6-
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg};
6+
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathEl, PathSeg};
77
use serde::{Deserialize, Serialize};
88

99
use crate::point::Point2D;
@@ -160,6 +160,46 @@ impl CubicBezier {
160160
closed: self.closed,
161161
}
162162
}
163+
164+
pub fn from_kurbo(path: &kurbo::BezPath) -> Self {
165+
let elems = path.elements();
166+
let mut res = Self {
167+
segments: Vec::with_capacity(elems.len() - 1),
168+
start: Point2D::default(),
169+
closed: false,
170+
};
171+
172+
for elem in elems {
173+
match elem {
174+
PathEl::MoveTo(p) => res.start = *p,
175+
PathEl::LineTo(p) => res.segments.push(CubicSegment::Line(*p)),
176+
PathEl::QuadTo(_, _) => {
177+
unimplemented!("quadratic segments are not supported in CubicBezier")
178+
}
179+
PathEl::CurveTo(c1, c2, p) => res.segments.push(CubicSegment::Curve(*c1, *c2, *p)),
180+
PathEl::ClosePath => res.closed = true,
181+
}
182+
}
183+
184+
res
185+
}
186+
187+
pub fn to_kurbo(&self) -> kurbo::BezPath {
188+
let mut path = kurbo::BezPath::new();
189+
190+
path.move_to(self.start);
191+
for seg in &self.segments {
192+
match seg {
193+
CubicSegment::Line(end) => path.line_to(*end),
194+
CubicSegment::Curve(c1, c2, end) => path.curve_to(*c1, *c2, *end),
195+
}
196+
}
197+
if self.closed {
198+
path.close_path();
199+
}
200+
201+
path
202+
}
163203
}
164204

165205
pub struct CubicBezierBuilder<P = Point2D> {

crates/monoxide-script/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ version.workspace = true
1111
by_address.workspace = true
1212
indexmap.workspace = true
1313
itertools.workspace = true
14+
linesweeper = "0.4.0"
1415
monoxide-curves.workspace = true
1516
monoxide-spiro.workspace = true
1617
monoxide-ttf.workspace = true

crates/monoxide-script/src/ast/simple.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use std::sync::Arc;
22

3+
use linesweeper::BinaryOp;
34
use monoxide_curves::{CubicBezier, SpiroCurve, point::Point2D, xform::Affine2D};
45

56
#[derive(Debug, Clone)]
67
pub enum OutlineExpr {
78
Bezier(CubicBezier),
89
Spiro(SpiroCurve),
10+
Bool(BinaryOp, Arc<OutlineExpr>, Arc<OutlineExpr>),
911
Stroked(Arc<OutlineExpr>, f64),
1012
Transformed(Arc<OutlineExpr>, Affine2D),
1113
}
@@ -24,4 +26,8 @@ impl OutlineExpr {
2426
pub fn transformed(self: Arc<Self>, xform: Affine2D) -> Arc<Self> {
2527
Arc::new(OutlineExpr::Transformed(self, xform))
2628
}
29+
30+
pub fn bool(self: Arc<Self>, other: Arc<Self>, op: BinaryOp) -> Arc<Self> {
31+
Arc::new(OutlineExpr::Bool(op, self, other))
32+
}
2733
}

crates/monoxide-script/src/dsl.rs

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
mod bezier_builder;
22
mod spiro_builder;
33

4-
use std::{iter, sync::Arc};
4+
use std::sync::Arc;
55

66
pub use bezier_builder::{BezierBuilder, BezierInst};
7-
use itertools::chain;
7+
use linesweeper::BinaryOp;
88
use monoxide_curves::xform::Affine2D;
99
pub use spiro_builder::{SpiroBuilder, SpiroInst, SpiroInstOpts};
1010

@@ -40,6 +40,38 @@ pub trait IntoOutlineExt: IntoOutline {
4040
{
4141
self.into_outline().transformed(xform)
4242
}
43+
44+
fn or<U: IntoOutline>(self, other: U) -> Arc<OutlineExpr>
45+
where
46+
Self: Sized,
47+
{
48+
self.into_outline()
49+
.bool(other.into_outline(), BinaryOp::Union)
50+
}
51+
52+
fn diff<U: IntoOutline>(self, other: U) -> Arc<OutlineExpr>
53+
where
54+
Self: Sized,
55+
{
56+
self.into_outline()
57+
.bool(other.into_outline(), BinaryOp::Difference)
58+
}
59+
60+
fn and<U: IntoOutline>(self, other: U) -> Arc<OutlineExpr>
61+
where
62+
Self: Sized,
63+
{
64+
self.into_outline()
65+
.bool(other.into_outline(), BinaryOp::Intersection)
66+
}
67+
68+
fn xor<U: IntoOutline>(self, other: U) -> Arc<OutlineExpr>
69+
where
70+
Self: Sized,
71+
{
72+
self.into_outline()
73+
.bool(other.into_outline(), BinaryOp::Xor)
74+
}
4375
}
4476

4577
impl<T: IntoOutline> IntoOutlineExt for T {}
@@ -86,30 +118,10 @@ pub trait IntoOutlinesExt: IntoOutlines {
86118
.into_iter()
87119
.map(move |outline| outline.transformed(xform))
88120
}
89-
90-
fn add<U: IntoOutlines>(self, other: U) -> Add<Self, U>
91-
where
92-
Self: Sized,
93-
{
94-
Add(self, other)
95-
}
96121
}
97122

98123
impl<T: IntoOutlines> IntoOutlinesExt for T {}
99124

100-
pub struct Add<T, U>(T, U);
101-
102-
impl<T: IntoOutlines, U: IntoOutlines> IntoOutlines for Add<T, U> {
103-
type Outlines = iter::Chain<
104-
<T::Outlines as IntoIterator>::IntoIter,
105-
<U::Outlines as IntoIterator>::IntoIter,
106-
>;
107-
108-
fn into_outlines(self) -> Self::Outlines {
109-
chain!(self.0.into_outlines(), self.1.into_outlines())
110-
}
111-
}
112-
113125
#[doc(hidden)]
114126
#[macro_export]
115127
macro_rules! ctrl_pt {

crates/monoxide-script/src/eval/outline.rs

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use itertools::{Itertools, chain};
2-
use monoxide_curves::{CubicBezier, SpiroCurve, stroke::StrokedSpiroCurve, xform::AffineExt};
2+
use linesweeper::{FillRule, binary_op};
3+
use monoxide_curves::{
4+
CubicBezier, SpiroCurve, convert::spiro_to_cube, stroke::StrokedSpiroCurve, xform::AffineExt,
5+
};
36

47
use crate::{ast::OutlineExpr, trace::EvalTracer};
58

@@ -21,8 +24,7 @@ pub fn eval_outline<E: EvalTracer>(
2124
let output_size_before = out.len();
2225
let id = dbg.spiro_to_bezier(evaled.id);
2326
for spiro in spiros {
24-
let bez = monoxide_curves::convert::spiro_to_cube(&spiro.points)
25-
.map_err(|e| EvalError::CurveError(id, e))?;
27+
let bez = spiro_to_cube(&spiro.points).map_err(|e| EvalError::CurveError(id, e))?;
2628
out.extend(bez);
2729
}
2830
dbg.intermediate_output(id, &out[output_size_before..]);
@@ -71,8 +73,8 @@ impl<Id: Copy> EvalValue<Id> {
7173
let mut beziers = vec![];
7274
let id = dbg.spiro_to_bezier(self.id);
7375
for spiro in &spiros {
74-
let bez = monoxide_curves::convert::spiro_to_cube(&spiro.points)
75-
.map_err(|e| EvalError::CurveError(id, e))?;
76+
let bez =
77+
spiro_to_cube(&spiro.points).map_err(|e| EvalError::CurveError(id, e))?;
7678
beziers.extend(bez);
7779
}
7880
dbg.intermediate_output(id, &beziers);
@@ -89,11 +91,27 @@ pub enum EvalError<Id> {
8991
)]
9092
StrokingABezier(Id),
9193

94+
#[error(
95+
"boolean operation on multiple beziers unimplemented at {0}: try using a single bezier"
96+
)]
97+
BoolingMultipleBeziers(Id),
98+
99+
#[error("boolean operation error at {0}: {1}")]
100+
Boolean(Id, #[source] linesweeper::Error),
101+
92102
#[error("curve evaluation error at {0}: {1}")]
93103
CurveError(Id, #[source] monoxide_curves::error::Error),
94104
}
95105

96106
fn eval_outline_internal<E: EvalTracer>(expr: &OutlineExpr, dbg: &mut E) -> EvalResult<E> {
107+
let to_bez = |spiro: &SpiroCurve, id, dbg: &mut E| {
108+
let bez = spiro_to_cube(&spiro.points).map_err(|e| EvalError::CurveError(id, e))?;
109+
if E::needs_evaluate_intermediate() {
110+
dbg.intermediate_output(id, &bez);
111+
}
112+
Ok(bez)
113+
};
114+
97115
match expr {
98116
OutlineExpr::Bezier(cubic_bezier) => {
99117
let id = dbg.constructed_bezier(cubic_bezier);
@@ -102,15 +120,42 @@ fn eval_outline_internal<E: EvalTracer>(expr: &OutlineExpr, dbg: &mut E) -> Eval
102120
OutlineExpr::Spiro(spiro) => {
103121
let id = dbg.constructed_spiro(&spiro.points);
104122

105-
if E::needs_evaluate_intermediate() {
106-
// convert to beziers if needed
107-
let bez = monoxide_curves::convert::spiro_to_cube(&spiro.points)
108-
.map_err(|e| EvalError::CurveError(id, e))?;
109-
dbg.intermediate_output(id, &bez);
110-
}
123+
// convert to beziers if needed
124+
to_bez(spiro, id, dbg)?;
111125

112126
Ok(EvalValue::spiro(spiro.clone(), id))
113127
}
128+
OutlineExpr::Bool(op, lhs, rhs) => {
129+
let mut eval_half = |outline| {
130+
let evaled = eval_outline_internal(outline, dbg)?;
131+
let bez = match evaled.kind {
132+
EvalValueKind::Beziers(bez) => Ok(bez),
133+
EvalValueKind::Spiros(spiro) => spiro
134+
.into_iter()
135+
.map(|s| to_bez(&s, evaled.id, dbg))
136+
.flatten_ok()
137+
.try_collect(),
138+
}?;
139+
let [bez] = &bez[..] else {
140+
return Err(EvalError::BoolingMultipleBeziers(evaled.id));
141+
};
142+
Ok((evaled.id, bez.to_kurbo()))
143+
};
144+
145+
let (lhs_id, lhs) = eval_half(lhs)?;
146+
let (rhs_id, rhs) = eval_half(rhs)?;
147+
148+
let id = dbg.boolean_added(&[lhs_id, rhs_id]);
149+
let merged = binary_op(&lhs, &rhs, FillRule::NonZero, *op)
150+
.map_err(|e| EvalError::Boolean(id, e))?
151+
.contours()
152+
.map(|it| CubicBezier::from_kurbo(&it.path))
153+
.collect();
154+
Ok(EvalValue {
155+
kind: EvalValueKind::Beziers(merged),
156+
id,
157+
})
158+
}
114159
OutlineExpr::Stroked(outline_expr, width) => {
115160
let evaled = eval_outline_internal(outline_expr, dbg)?;
116161
match evaled.kind {
@@ -176,10 +221,7 @@ fn eval_stroked<E: EvalTracer>(
176221
if E::needs_evaluate_intermediate() {
177222
// Convert both original spiro and the stroked spiro to beziers
178223
let bezs: Vec<_> = chain!(eval_spiros, &out_spiros)
179-
.map(|spiro| {
180-
monoxide_curves::convert::spiro_to_cube(&spiro.points)
181-
.map_err(|e| EvalError::CurveError(id, e))
182-
})
224+
.map(|spiro| spiro_to_cube(&spiro.points).map_err(|e| EvalError::CurveError(id, e)))
183225
.flatten_ok()
184226
.try_collect()?;
185227
dbg.intermediate_output(id, &bezs);

0 commit comments

Comments
 (0)