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
33struct Lengths {
35 length_bass: f64,
36 length_treble: f64,
37}
38
39struct Point(pub f64, pub f64);
41
42struct Line {
44 start: Point,
45 end: Point,
46}
47
48impl Lengths {
49 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 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 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 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))]
115pub struct Specs {
117 pub scale: f64,
119 pub count: u32,
121 pub variant: Variant,
123 pub nut: f64,
125 pub bridge: f64,
129 factors: Factors,
130}
131
132impl Default for Specs {
133 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 #[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 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 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 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 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 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 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 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 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 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 #[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 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
471pub 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}