fretboard_layout/
lib.rs

1#![warn(clippy::all, clippy::pedantic)]
2#![allow(clippy::must_use_candidate)]
3#![doc = include_str!("../README.md")]
4
5mod config;
6mod factors;
7mod handedness;
8pub mod open;
9mod variant;
10
11pub use {
12    config::{
13        font::{Font, Weight},
14        Config, Units,
15    },
16    factors::Factors,
17    handedness::{Handedness, ParseHandednessError},
18    rgba_simple::*,
19    variant::{MultiscaleBuilder, Variant},
20};
21
22use {
23    rayon::prelude::*,
24    svg::{
25        node::element::{path::Data, Description, Group, Path, Text},
26        Document,
27    },
28};
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33/// Distance from bridge to fret along each side of the fretboard.
34struct Lengths {
35    length_bass: f64,
36    length_treble: f64,
37}
38
39/// A 2-dimensional representation of a point
40struct Point(pub f64, pub f64);
41
42/// 2 Points which form a line
43struct Line {
44    start: Point,
45    end: Point,
46}
47
48impl Lengths {
49    /// Plots the end of a fret, nut or bridge along the bass side of the scale
50    fn get_point_bass(&self, specs: &Specs, config: &Config) -> Point {
51        let hand = specs.variant.handedness();
52        let x = match hand {
53            Some(Handedness::Left) => {
54                specs.scale - (specs.factors.x_ratio * self.length_bass) + config.border
55            }
56            _ => (specs.factors.x_ratio * self.length_bass) + config.border,
57        };
58        let opposite = specs.factors.y_ratio * self.length_bass;
59        let y = opposite + config.border;
60        Point(x, y)
61    }
62
63    /// Plots the end of a fret, nut or bridge along the treble side of the scale
64    fn get_point_treble(&self, specs: &Specs, config: &Config) -> Point {
65        let hand = specs.variant.handedness();
66        let x = match hand {
67            Some(Handedness::Left) => {
68                specs.scale + config.border
69                    - specs.factors.treble_offset
70                    - (specs.factors.x_ratio * self.length_treble)
71            }
72            _ => {
73                specs.factors.treble_offset
74                    + (specs.factors.x_ratio * self.length_treble)
75                    + config.border
76            }
77        };
78        let opposite = specs.factors.y_ratio * self.length_treble;
79        let y = specs.bridge - opposite + config.border;
80        Point(x, y)
81    }
82
83    /// Returns a Point struct containing both ends of a fret, nut or bridge
84    /// which will form a line
85    fn get_fret_line(&self, specs: &Specs, config: &Config) -> Line {
86        let start = self.get_point_bass(specs, config);
87        let end = self.get_point_treble(specs, config);
88        Line { start, end }
89    }
90}
91
92impl Line {
93    /// Returns an svg Path node representing a single fret
94    fn draw_fret(&self, fret: u32, config: &Config) -> Path {
95        let id = if fret == 0 {
96            "Nut".to_string()
97        } else {
98            format!("Fret {fret}")
99        };
100        let data = Data::new()
101            .move_to((self.start.0, self.start.1))
102            .line_to((self.end.0, self.end.1))
103            .close();
104        Path::new()
105            .set("fill", "none")
106            .set("stroke", config.fretline_color.to_hex())
107            .set("stroke-opacity", config.fretline_color.alpha)
108            .set("stroke-width", config.line_weight)
109            .set("id", id)
110            .set("d", data)
111    }
112}
113
114#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
115/// This struct contains the user data used to create the svg output file
116pub struct Specs {
117    /// Scale length. For multiscale designs this is the bass side scale length.
118    pub scale: f64,
119    /// Number of frets to render
120    pub count: u32,
121    /// Monoscale or Multiscale Right orLeft handed
122    pub variant: Variant,
123    /// The width of the fretboard at the nut.
124    pub nut: f64,
125    /// The string spacing at the bridge. Note that this is not the physical
126    /// width of the bridge, but the distance perpendicular to the centerline
127    /// between the outer two strings.
128    pub bridge: f64,
129    factors: Factors,
130}
131
132impl Default for Specs {
133    /// Returns a default Specs struct
134    fn default() -> Self {
135        Self::init(655.0, 24, Variant::default(), 43.0, 56.0)
136    }
137}
138
139impl Specs {
140    #[must_use]
141    pub fn init(scale: f64, count: u32, variant: Variant, nut: f64, bridge: f64) -> Self {
142        let factors = Factors::init(scale, &variant, nut, bridge);
143        Self {
144            scale,
145            count,
146            variant,
147            nut,
148            bridge,
149            factors,
150        }
151    }
152
153    pub fn builder() -> SpecsBuilder {
154        SpecsBuilder::new()
155    }
156
157    /// Returns a multiscale Specs struct
158    #[allow(clippy::must_use_candidate)]
159    pub fn multi() -> Self {
160        Self::init(655.0, 24, Variant::multi(), 43.0, 56.0)
161    }
162
163    #[allow(clippy::must_use_candidate)]
164    pub fn scale(&self) -> f64 {
165        self.scale
166    }
167
168    pub fn set_scale(&mut self, scale: f64) {
169        self.scale = scale;
170    }
171
172    #[allow(clippy::must_use_candidate)]
173    pub fn count(&self) -> u32 {
174        self.count
175    }
176
177    pub fn set_count(&mut self, count: u32) {
178        self.count = count;
179    }
180
181    #[allow(clippy::must_use_candidate)]
182    pub fn variant(&self) -> Variant {
183        self.variant
184    }
185
186    pub fn set_multi(&mut self, scale: Option<f64>, pfret: Option<f64>) {
187        match scale {
188            Some(s) => {
189                if let Some(hand) = self.variant.handedness() {
190                    self.variant = Variant::Multiscale {
191                        scale: s,
192                        handedness: hand,
193                        pfret: pfret.unwrap_or(8.0),
194                    };
195                } else {
196                    self.variant = Variant::Multiscale {
197                        scale: s,
198                        handedness: Handedness::Right,
199                        pfret: pfret.unwrap_or(8.0),
200                    };
201                };
202            }
203            None => self.variant = Variant::Monoscale,
204        }
205    }
206
207    #[allow(clippy::must_use_candidate)]
208    pub fn nut(&self) -> f64 {
209        self.nut
210    }
211
212    pub fn set_nut(&mut self, nut: f64) {
213        self.nut = nut;
214    }
215
216    #[allow(clippy::must_use_candidate)]
217    pub fn bridge(&self) -> f64 {
218        self.bridge
219    }
220
221    pub fn set_bridge(&mut self, bridge: f64) {
222        self.bridge = bridge;
223    }
224
225    /// Returns the distance from bridge to nut on both sides of the fretboard
226    fn get_nut(&self) -> Lengths {
227        let length_treble = match self.variant {
228            Variant::Multiscale { scale: s, .. } => s,
229            Variant::Monoscale => self.scale,
230        };
231        Lengths {
232            length_bass: self.scale,
233            length_treble,
234        }
235    }
236
237    /// Returns the length from bridge to fret for a given fret number, along
238    /// both bass and treble sides of the board.
239    fn get_fret_lengths(&self, fret: u32) -> Lengths {
240        let factor = 2.0_f64.powf(f64::from(fret) / 12.0);
241        let length_bass = self.scale / factor;
242        let length_treble = match self.variant {
243            Variant::Monoscale => length_bass,
244            Variant::Multiscale { scale: s, .. } => s / factor,
245        };
246        Lengths {
247            length_bass,
248            length_treble,
249        }
250    }
251
252    /// Embeds a text description into the svg
253    fn create_description(&self) -> Description {
254        let desc = Description::new()
255            .set("Scale", self.scale)
256            .set("BridgeSpacing", self.bridge - 6.0)
257            .set("NutWidth", self.nut)
258            .set("FretCount", self.count);
259        match self.variant {
260            Variant::Multiscale {
261                scale: scl,
262                handedness: hnd,
263                pfret: pf,
264            } => desc
265                .set("ScaleTreble", scl)
266                .set("PerpendicularFret", pf)
267                .set("Handedness", hnd.to_string()),
268            Variant::Monoscale => desc,
269        }
270    }
271
272    /// Prints the specs used in the rendered image
273    fn print_data(&self, config: &Config) -> Text {
274        let units = match config.units {
275            Units::Metric => String::from("mm"),
276            Units::Imperial => String::from("in"),
277        };
278        let mut line = match self.variant {
279            Variant::Monoscale => format!("Scale: {:.2}{} |", self.scale, &units),
280            Variant::Multiscale {
281                scale: s, pfret: f, ..
282            } => format!(
283                "ScaleBass: {:.2}{} | ScaleTreble: {s:.2}{} | PerpendicularFret: {f:.1} |",
284                self.scale, &units, &units
285            ),
286        };
287        let font = config.font.clone().unwrap_or_default();
288        let font_size = match config.units {
289            Units::Metric => "5px",
290            Units::Imperial => "0.25px",
291        };
292        line = format!("{line} NutWidth: {:.2}{} |", self.nut, &units);
293        let bridge = match config.units {
294            Units::Metric => self.bridge - 6.0,
295            Units::Imperial => self.bridge - (6.0 / 20.4),
296        };
297        line = format!("{line} BridgeSpacing: {bridge:.2}{}", &units);
298        svg::node::element::Text::new()
299            .set("x", config.border)
300            .set("y", (config.border * 1.7) + self.bridge)
301            .set("font-family", font.family())
302            .set("font-weight", font.weight().css_value())
303            .set("font-stretch", font.stretch().css_value())
304            .set("font-style", font.style().css_value())
305            .set("font-size", font_size)
306            .set("id", "Specifications")
307            .add(svg::node::Text::new(line))
308    }
309
310    /// Adds the centerline to the svg data
311    fn draw_centerline(&self, config: &Config) -> Path {
312        let start_x = config.border;
313        let start_y = (self.bridge / 2.0) + config.border;
314        let end_x = config.border + self.scale;
315        let end_y = (self.bridge / 2.0) + config.border;
316        let (hex, opacity) = match &config.centerline_color {
317            Some(c) => (c.to_hex(), f32::from(c.alpha) * 255.0),
318            None => (RGBA::<u8>::from(PrimaryColor::Blue).to_hex(), 1.0),
319        };
320        let dasharray = match config.units {
321            Units::Metric => "4.0, 8.0",
322            Units::Imperial => "0.2, 0.4",
323        };
324        let data = Data::new()
325            .move_to((start_x, start_y))
326            .line_to((end_x, end_y))
327            .close();
328        Path::new()
329            .set("fill", "none")
330            .set("stroke", hex)
331            .set("stroke-opacity", opacity)
332            .set("stroke-dasharray", dasharray)
333            .set("stroke-dashoffset", "0")
334            .set("stroke-width", config.line_weight)
335            .set("id", "Centerline")
336            .set("d", data)
337    }
338
339    /// adds the bridge as a line between the outer strings
340    fn draw_bridge(&self, config: &Config) -> Path {
341        let start_x = match self.variant {
342            Variant::Monoscale
343            | Variant::Multiscale {
344                handedness: Handedness::Right,
345                ..
346            } => config.border,
347            Variant::Multiscale {
348                handedness: Handedness::Left,
349                ..
350            } => config.border + self.scale,
351        };
352        let start_y = config.border;
353        let end_x = match self.variant {
354            Variant::Monoscale
355            | Variant::Multiscale {
356                handedness: Handedness::Right,
357                ..
358            } => config.border + self.factors.treble_offset,
359            Variant::Multiscale {
360                handedness: Handedness::Left,
361                ..
362            } => config.border + self.scale - self.factors.treble_offset,
363        };
364        let end_y = config.border + self.bridge;
365        let data = Data::new()
366            .move_to((start_x, start_y))
367            .line_to((end_x, end_y))
368            .close();
369        Path::new()
370            .set("fill", "none")
371            .set("stroke", "black")
372            .set("stroke-width", config.line_weight)
373            .set("id", "Bridge")
374            .set("d", data)
375    }
376
377    /// Draws the outline of the fretboard
378    fn draw_fretboard(&self, config: &Config) -> Path {
379        let nut = self.get_nut().get_fret_line(self, config);
380        let end = self
381            .get_fret_lengths(self.count + 1)
382            .get_fret_line(self, config);
383        let (hex, alpha) = (
384            config.fretboard_color.to_hex(),
385            config.fretboard_color.alpha,
386        );
387        let data = Data::new()
388            .move_to((nut.start.0, nut.start.1))
389            .line_to((nut.end.0, nut.end.1))
390            .line_to((end.end.0, end.end.1))
391            .line_to((end.start.0, end.start.1))
392            .line_to((nut.start.0, nut.start.1))
393            .close();
394        Path::new()
395            .set("fill", hex)
396            .set("fill-opacity", alpha)
397            .set("stroke", "none")
398            .set("id", "Fretboard")
399            .set("d", data)
400    }
401
402    /// draws a single fret
403    fn draw_fret(&self, config: &Config, num: u32) -> Path {
404        self.get_fret_lengths(num)
405            .get_fret_line(self, config)
406            .draw_fret(num, config)
407    }
408
409    /// Iterates through each fret, returning a group of svg Paths
410    fn draw_frets(&self, cfg: &Config) -> Group {
411        let frets = Group::new().set("id", "Frets");
412        let f: Vec<Path> = (0..=self.count)
413            .into_par_iter()
414            .map(|fret| self.draw_fret(cfg, fret))
415            .collect();
416        f.into_iter().fold(frets, Group::add)
417    }
418
419    ///Returns the complete svg Document
420    ///# Example
421    ///
422    ///```rust
423    ///use fretboard_layout::{Config, Specs};
424    ///
425    ///fn run() {
426    ///    let specs = Specs::default();
427    ///    let doc = specs.create_document(Some(Config::default()));
428    ///}
429    ///```
430    #[must_use]
431    pub fn create_document(&self, conf: Option<Config>) -> svg::Document {
432        let config = conf.unwrap_or_default();
433        let width = (config.border * 2.0) + self.scale;
434        let units = match config.units {
435            Units::Metric => "mm",
436            Units::Imperial => "in",
437        };
438        let widthmm = format!("{width}{units}");
439        let height = (config.border * 2.0) + self.bridge;
440        let heightmm = format!("{height}{units}");
441        // Todo - investigate generating these values async
442        let description = self.create_description();
443        let fretboard = self.draw_fretboard(&config);
444        let bridge = self.draw_bridge(&config);
445        let frets = self.draw_frets(&config);
446        let document = Document::new()
447            .set("width", widthmm)
448            .set("height", heightmm)
449            .set("preserveAspectRatio", "xMidYMid meet")
450            .set("viewBox", (0, 0, width, height))
451            .add(description)
452            .add(fretboard)
453            .add(bridge)
454            .add(frets);
455        if config.font.is_some() {
456            if config.centerline_color.is_some() {
457                document
458                    .add(self.print_data(&config))
459                    .add(self.draw_centerline(&config))
460            } else {
461                document.add(self.print_data(&config))
462            }
463        } else if config.centerline_color.is_some() {
464            document.add(self.draw_centerline(&config))
465        } else {
466            document
467        }
468    }
469}
470
471/// A Specs builder
472pub struct SpecsBuilder {
473    scale: f64,
474    count: u32,
475    variant: Variant,
476    nut: f64,
477    bridge: f64,
478}
479
480impl Default for SpecsBuilder {
481    fn default() -> Self {
482        Self {
483            scale: 655.0,
484            count: 24,
485            variant: Variant::Monoscale,
486            nut: 43.0,
487            bridge: 56.0,
488        }
489    }
490}
491
492impl SpecsBuilder {
493    #[must_use]
494    pub fn new() -> Self {
495        Self::default()
496    }
497
498    #[must_use]
499    pub fn scale(mut self, scale: f64) -> Self {
500        self.scale = scale;
501        self
502    }
503
504    #[must_use]
505    pub fn count(mut self, count: u32) -> Self {
506        self.count = count;
507        self
508    }
509
510    #[must_use]
511    pub fn variant(mut self, variant: Variant) -> Self {
512        self.variant = variant;
513        self
514    }
515
516    #[must_use]
517    pub fn nut(mut self, nut: f64) -> Self {
518        self.nut = nut;
519        self
520    }
521
522    #[must_use]
523    pub fn bridge(mut self, bridge: f64) -> Self {
524        self.bridge = bridge;
525        self
526    }
527
528    #[must_use]
529    pub fn build(self) -> Specs {
530        Specs::init(self.scale, self.count, self.variant, self.nut, self.bridge)
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn lengths() {
540        let specs = Specs::default();
541        let lengths = specs.get_fret_lengths(12);
542        assert_eq!(lengths.length_bass, 327.5);
543        assert_eq!(lengths.length_treble, lengths.length_treble);
544        let lengths = specs.get_fret_lengths(24);
545        assert_eq!(lengths.length_bass, 163.75);
546        assert_eq!(lengths.length_bass, lengths.length_treble);
547    }
548}