Skip to main content

roughr/
core.rs

1use euclid::default::Point2D;
2use euclid::Trig;
3use num_traits::{Float, FromPrimitive};
4use palette::Srgba;
5use rand::random;
6use std::fmt;
7
8pub struct Space;
9
10pub struct Config {
11    #[allow(dead_code)]
12    options: Option<Options>,
13}
14
15pub struct DrawingSurface {
16    #[allow(dead_code)]
17    width: f32,
18    #[allow(dead_code)]
19    height: f32,
20}
21
22#[derive(Clone, PartialEq, Debug, Copy, Eq)]
23pub enum FillStyle {
24    Solid,
25    Hachure,
26    ZigZag,
27    CrossHatch,
28    Dots,
29    Dashed,
30    ZigZagLine,
31}
32
33impl fmt::Display for FillStyle {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        let s = match self {
36            FillStyle::Solid => "Solid",
37            FillStyle::Hachure => "Hachure",
38            FillStyle::ZigZag => "ZigZag",
39            FillStyle::CrossHatch => "CrossHatch",
40            FillStyle::Dots => "Dots",
41            FillStyle::Dashed => "Dashed",
42            FillStyle::ZigZagLine => "ZigZagLine",
43        };
44        f.write_str(s)
45    }
46}
47
48#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
49pub enum LineCap {
50    #[default]
51    Butt,
52    Round,
53    Square,
54}
55
56/// Options for angled joins in strokes.
57#[derive(Clone, Copy, PartialEq, Debug)]
58pub enum LineJoin {
59    Miter { limit: f64 },
60    Round,
61    Bevel,
62}
63impl LineJoin {
64    pub const DEFAULT_MITER_LIMIT: f64 = 10.0;
65}
66impl Default for LineJoin {
67    fn default() -> Self {
68        LineJoin::Miter {
69            limit: LineJoin::DEFAULT_MITER_LIMIT,
70        }
71    }
72}
73
74#[derive(Clone, Builder)]
75#[builder(setter(strip_option))]
76pub struct Options {
77    #[builder(default = "Some(2.0)")]
78    pub max_randomness_offset: Option<f32>,
79    #[builder(default = "Some(1.0)")]
80    pub roughness: Option<f32>,
81    #[builder(default = "Some(2.0)")]
82    pub bowing: Option<f32>,
83    #[builder(default = "Some(Srgba::new(0.0, 0.0, 0.0, 1.0))")]
84    pub stroke: Option<Srgba>,
85    #[builder(default = "Some(1.0)")]
86    pub stroke_width: Option<f32>,
87    #[builder(default = "Some(0.95)")]
88    pub curve_fitting: Option<f32>,
89    #[builder(default = "Some(0.0)")]
90    pub curve_tightness: Option<f32>,
91    #[builder(default = "Some(9.0)")]
92    pub curve_step_count: Option<f32>,
93    #[builder(default = "None")]
94    pub fill: Option<Srgba>,
95    #[builder(default = "None")]
96    pub fill_style: Option<FillStyle>,
97    #[builder(default = "Some(-1.0)")]
98    pub fill_weight: Option<f32>,
99    #[builder(default = "Some(-41.0)")]
100    pub hachure_angle: Option<f32>,
101    #[builder(default = "Some(-1.0)")]
102    pub hachure_gap: Option<f32>,
103    #[builder(default = "Some(1.0)")]
104    pub simplification: Option<f32>,
105    #[builder(default = "Some(-1.0)")]
106    pub dash_offset: Option<f32>,
107    #[builder(default = "Some(-1.0)")]
108    pub dash_gap: Option<f32>,
109    #[builder(default = "Some(-1.0)")]
110    pub zigzag_offset: Option<f32>,
111    #[builder(default = "Some(0_u64)")]
112    pub seed: Option<u64>,
113    #[builder(default = "None")]
114    pub stroke_line_dash: Option<Vec<f64>>,
115    #[builder(default = "None")]
116    pub stroke_line_dash_offset: Option<f64>,
117    #[builder(default = "None")]
118    pub line_cap: Option<LineCap>,
119    #[builder(default = "None")]
120    pub line_join: Option<LineJoin>,
121    #[builder(default = "None")]
122    pub fill_line_dash: Option<Vec<f64>>,
123    #[builder(default = "None")]
124    pub fill_line_dash_offset: Option<f64>,
125    #[builder(default = "Some(false)")]
126    pub disable_multi_stroke: Option<bool>,
127    #[builder(default = "Some(false)")]
128    pub disable_multi_stroke_fill: Option<bool>,
129    #[builder(default = "Some(false)")]
130    pub preserve_vertices: Option<bool>,
131    #[builder(default = "None")]
132    pub fixed_decimal_place_digits: Option<f32>,
133    // Rough.js stores the evolving PRNG state in `ops.randomizer` (not in `ops.seed`).
134    // This is internal-only and must not be user-set.
135    #[builder(default = "None", setter(skip))]
136    pub(crate) randomizer: Option<i32>,
137}
138
139impl Default for Options {
140    fn default() -> Self {
141        Options {
142            max_randomness_offset: Some(2.0),
143            roughness: Some(1.0),
144            bowing: Some(1.0),
145            stroke: Some(Srgba::new(0.0, 0.0, 0.0, 1.0)),
146            stroke_width: Some(1.0),
147            curve_tightness: Some(0.0),
148            curve_fitting: Some(0.95),
149            curve_step_count: Some(9.0),
150            fill: None,
151            fill_style: None,
152            fill_weight: Some(-1.0),
153            hachure_angle: Some(-41.0),
154            hachure_gap: Some(-1.0),
155            dash_offset: Some(-1.0),
156            dash_gap: Some(-1.0),
157            zigzag_offset: Some(-1.0),
158            seed: Some(0_u64),
159            disable_multi_stroke: Some(false),
160            disable_multi_stroke_fill: Some(false),
161            preserve_vertices: Some(false),
162            simplification: Some(1.0),
163            stroke_line_dash: None,
164            stroke_line_dash_offset: None,
165            line_cap: None,
166            line_join: None,
167            fill_line_dash: None,
168            fill_line_dash_offset: None,
169            fixed_decimal_place_digits: None,
170            randomizer: None,
171        }
172    }
173}
174
175impl Options {
176    pub fn random(&mut self) -> f64 {
177        // Match Rough.js `random(ops)` in `bin/renderer.js`:
178        //
179        // - `ops.seed` is the *base seed* (stable across calls).
180        // - `ops.randomizer` is lazily created and holds the evolving 32-bit state.
181        // - If seed is `0` (falsy), `Random.next()` falls back to `Math.random()` without
182        //   advancing state (but `ops.randomizer` still exists and stays falsy).
183        if self.randomizer.is_none() {
184            let seed_bits = self.seed.unwrap_or(0) as u32;
185            self.randomizer = Some(seed_bits as i32);
186        }
187
188        let state = self.randomizer.unwrap_or(0);
189        if state != 0 {
190            // Match Rough.js `Random.next()` from `bin/math.js`:
191            //
192            // `return ((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31;`
193            //
194            // - `Math.imul` is a signed 32-bit multiply
195            // - assignment stores the raw signed 32-bit result
196            // - returned value is masked with `& 0x7fffffff`
197            let next = state.wrapping_mul(48271);
198            self.randomizer = Some(next);
199            let out = next & 0x7fffffff;
200            return (out as f64) / 2147483648.0;
201        }
202
203        random::<f64>()
204    }
205
206    pub fn set_hachure_angle(&mut self, angle: Option<f32>) -> &mut Self {
207        self.hachure_angle = angle;
208        self
209    }
210
211    pub fn set_hachure_gap(&mut self, gap: Option<f32>) -> &mut Self {
212        self.hachure_gap = gap;
213        self
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::{Options, OptionsBuilder};
220
221    #[test]
222    fn roughjs_random_seed_1_matches_known_sequence() {
223        // Matches Rough.js `Random.next()` from `bin/math.js` with `seed = 1`.
224        let denom = 2147483648.0_f64; // 2^31
225        let expected_out: [u32; 10] = [
226            48_271,
227            182_605_793,
228            1_291_342_511,
229            1_533_981_633,
230            1_591_223_503,
231            902_075_297,
232            1_698_214_639,
233            773_027_713,
234            144_866_575,
235            647_683_937,
236        ];
237        let expected: Vec<f64> = expected_out.iter().map(|&n| (n as f64) / denom).collect();
238
239        let mut opts: Options = OptionsBuilder::default().seed(1_u64).build().unwrap();
240        let got: Vec<f64> = (0..expected.len()).map(|_| opts.random()).collect();
241
242        assert_eq!(got, expected);
243    }
244}
245
246#[derive(Clone, PartialEq, Debug, Eq)]
247pub enum OpType {
248    Move,
249    BCurveTo,
250    LineTo,
251}
252
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub enum OpSetType {
255    Path,
256    FillPath,
257    FillSketch,
258}
259
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct Op<F: Float + Trig> {
262    pub op: OpType,
263    pub data: Vec<F>,
264}
265
266#[derive(Clone, Debug, PartialEq, Eq)]
267pub struct OpSet<F: Float + Trig> {
268    pub op_set_type: OpSetType,
269    pub ops: Vec<Op<F>>,
270    pub size: Option<Point2D<F>>,
271    pub path: Option<String>,
272}
273
274pub struct Drawable<F: Float + Trig> {
275    pub shape: String,
276    pub options: Options,
277    pub sets: Vec<OpSet<F>>,
278}
279
280pub struct PathInfo {
281    pub d: String,
282    pub stroke: Option<Srgba>,
283    pub stroke_width: Option<f32>,
284    pub fill: Option<Srgba>,
285}
286
287pub fn _c<U: Float + FromPrimitive>(inp: f32) -> U {
288    U::from(inp).expect("can not parse from f32")
289}
290
291pub fn _cc<U: Float + FromPrimitive>(inp: f64) -> U {
292    U::from(inp).expect("can not parse from f64")
293}