Address review feedback Mostly added comments and a bit of renaming, no logic changes.
diff --git a/shader/flatten.wgsl b/shader/flatten.wgsl index 59335d8..3f1a855 100644 --- a/shader/flatten.wgsl +++ b/shader/flatten.wgsl
@@ -218,9 +218,9 @@ } // Note: offset provided is scaled so that 1 = chord length -fn es_seg_eval_with_offset(es: EulerSeg, t: f32, offset: f32) -> vec2f { +fn es_seg_eval_with_offset(es: EulerSeg, t: f32, normalized_offset: f32) -> vec2f { let chord = es.p1 - es.p0; - let xy = es_params_eval_with_offset(es.params, t, offset); + let xy = es_params_eval_with_offset(es.params, t, normalized_offset); return es.p0 + vec2f(chord.x * xy.x - chord.y * xy.y, chord.x * xy.y + chord.y * xy.x); } @@ -403,8 +403,8 @@ let es = es_seg_from_params(this_p0, this_pq1.point, euler_params); let k0 = es.params.k0 - 0.5 * es.params.k1; let k1 = es.params.k1; - let offset_chord_normalized = offset / cubic_params.chord_len; - let dist_scaled = offset_chord_normalized * es.params.ch; + let normalized_offset = offset / cubic_params.chord_len; + let dist_scaled = normalized_offset * es.params.ch; let scale_multiplier = sqrt(0.125 * scale * cubic_params.chord_len / (es.params.ch * tol)); var a = 0.0; var b = 0.0; @@ -452,7 +452,7 @@ } s = (inv - b) / a; } - lp1 = es_seg_eval_with_offset(es, s, offset_chord_normalized); + lp1 = es_seg_eval_with_offset(es, s, normalized_offset); } let l0 = select(lp1, lp0, offset >= 0.); let l1 = select(lp0, lp1, offset >= 0.);
diff --git a/src/cpu_shader/euler.rs b/src/cpu_shader/euler.rs index 575ed5b..84cf254 100644 --- a/src/cpu_shader/euler.rs +++ b/src/cpu_shader/euler.rs
@@ -72,7 +72,10 @@ let chord = p1 - p0; let chord_squared = chord.length_squared(); let chord_len = chord_squared.sqrt(); + // Chord is near-zero; straight line case. if chord_squared < TANGENT_THRESH.powi(2) { + // This error estimate was determined empirically through randomized + // testing, though it is likely it can be derived analytically. let chord_err = ((9. / 32.0) * (q0.length_squared() + q1.length_squared())).sqrt() * dt; return CubicParams { th0: 0.0, @@ -205,6 +208,7 @@ Vec2::new(x, y) } + // Offset provided is in same units as curve; chord is normalized to (1, 0). fn eval_with_offset(&self, t: f32, offset: f32) -> Vec2 { let th = self.eval_th(t); let v = Vec2::new(offset * th.sin(), offset * th.cos()); @@ -227,10 +231,11 @@ ) } - // Note: offset provided is scaled so that 1 = chord length - pub fn eval_with_offset(&self, t: f32, offset: f32) -> Vec2 { + // Note: offset provided is normalized so that 1 = chord length, while + // the return value is in the same coordinate space as the endpoints. + pub fn eval_with_offset(&self, t: f32, normalized_offset: f32) -> Vec2 { let chord = self.p1 - self.p0; - let Vec2 { x, y } = self.params.eval_with_offset(t, offset); + let Vec2 { x, y } = self.params.eval_with_offset(t, normalized_offset); Vec2::new( self.p0.x + chord.x * x - chord.y * y, self.p0.y + chord.x * y + chord.y * x,
diff --git a/src/cpu_shader/flatten.rs b/src/cpu_shader/flatten.rs index d43837c..1fa08c7 100644 --- a/src/cpu_shader/flatten.rs +++ b/src/cpu_shader/flatten.rs
@@ -21,6 +21,12 @@ }}; } +// Note to readers: this file contains sophisticated techniques for expanding stroke +// outlines to flattened filled outlines, based on Euler spirals as an intermediate +// curve representation. In some cases, there are explanatory comments in the +// corresponding `cpu_shaders/` files (`flatten.rs` and the supporting `euler.rs`). +// A paper is in the works explaining the techniques in more detail. + /// Threshold below which a derivative is considered too small. const DERIV_THRESH: f32 = 1e-6; /// Amount to nudge t when derivative is near-zero. @@ -259,7 +265,7 @@ if t0 == 1. { break; } - log!("@@@ loop1: t0: {t0}, dt: {dt}"); + log!("@@@ loop start: t0: {t0}, dt: {dt}"); let mut t1 = t0 + dt; let this_p0 = last_p; let this_q0 = last_q; @@ -277,7 +283,7 @@ let actual_dt = t1 - last_t; let cubic_params = CubicParams::from_points_derivs(this_p0, this_p1, this_q0, this_q1, actual_dt); - log!("@@@ loop2: sub:{:?}, {:?} t0: {t0}, t1: {t1}, dt: {dt}, est_err: {est_err}, err: {err}", subcubic, cubic_params); + log!("@@@ loop: p0={this_p0:?} p1={this_p1:?} q0={this_q0:?} q1={this_q1:?} {cubic_params:?} t0: {t0}, t1: {t1}, dt: {dt}"); if cubic_params.err * scale <= tol || dt <= SUBDIV_LIMIT { log!("@@@ error within tolerance"); let euler_params = EulerParams::from_angles(cubic_params.th0, cubic_params.th1); @@ -286,8 +292,8 @@ let (k0, k1) = (es.params.k0 - 0.5 * es.params.k1, es.params.k1); // compute forward integral to determine number of subdivisions - let offset_chord_normalized = offset / cubic_params.chord_len; - let dist_scaled = offset_chord_normalized * es.params.ch; + let normalized_offset = offset / cubic_params.chord_len; + let dist_scaled = normalized_offset * es.params.ch; // The number of subdivisions for curvature = 1 let scale_multiplier = 0.5 * FRAC_1_SQRT_2 @@ -328,7 +334,7 @@ let n = (n_frac * scale_multiplier).ceil().max(1.0); // Flatten line segments - log!("@@@ loop2: lines: {n}"); + log!("@@@ loop: lines: {n}"); assert!(!n.is_nan()); for i in 0..n as usize { let lp1 = if i == n as usize - 1 && t1 == 1.0 { @@ -348,7 +354,7 @@ (inv - b) / a } }; - es.eval_with_offset(s, offset_chord_normalized) + es.eval_with_offset(s, normalized_offset) }; let l0 = if offset >= 0. { lp0 } else { lp1 }; let l1 = if offset >= 0. { lp1 } else { lp0 }; @@ -358,11 +364,16 @@ last_p = this_p1; last_q = this_q1; last_t = t1; + // Advance segment to next range. Beginning of segment is the end of + // this one. The number of trailing zeros represents the number of stack + // frames to pop in the recursive version of adaptive subdivision, and + // each stack pop represents doubling of the size of the range. t0_u += 1; let shift = t0_u.trailing_zeros(); t0_u >>= shift; dt *= (1 << shift) as f32; } else { + // Subdivide; halve the size of the range while retaining its start. t0_u = t0_u.saturating_mul(2); dt *= 0.5; }