adic_shape/shape/
clock_shape.rs

1use std::{
2    f64::consts::TAU,
3    iter::repeat,
4};
5use adic::AdicInteger;
6
7use crate::{AdicShapeError, DisplayShape};
8use super::element::{
9    AdicEl,
10    PathDInstruction,
11    CircleEl, PathEl, TextEl,
12};
13
14
15type Coordinate = (f64, f64);
16
17#[derive(Debug, Clone)]
18/// Clock shape for drawing
19///
20/// ```
21/// # use adic::radic;
22/// # use adic_shape::{ClockShape, ClockShapeOptions};
23/// let a = radic!(5, [1, 2, 3, 4], [0, 3]);
24/// let depth = 20;
25/// let shape_options = ClockShapeOptions{
26///     show_hand_radii: false,
27///     ..Default::default()
28/// };
29/// let clock_shape = ClockShape::new(&a, depth, shape_options);
30/// ```
31#[doc = ""]
32#[doc = "<style>"]
33#[doc = include_str!("../../img/rustdoc.css")]
34#[doc = "</style>"]
35#[doc = ""]
36#[doc = include_str!("../../img/clock-shape-example.svg")]
37#[doc = ""]
38pub struct ClockShape {
39    /// Number of ticks on the clock
40    p: u32,
41    /// Options for creating `ClockShape`
42    options: ClockShapeOptions,
43    /// Clock position
44    position: ClockPosition,
45    /// Clock hand information
46    clock_hands: Vec<ClockHand>,
47}
48
49
50#[derive(Debug, Clone, Copy)]
51/// Options for creating tree shape
52pub struct ClockShapeOptions {
53    /// Ticking or sweeping clock hands
54    pub clock_movement: ClockMovement,
55    /// Enable to show dotted lines at the radius of the clock hand heads
56    pub show_hand_radii: bool,
57    /// Width of the clock window
58    pub viewbox_width: u32,
59    /// Height of the clock window
60    pub viewbox_height: u32,
61}
62
63impl Default for ClockShapeOptions {
64    fn default() -> Self {
65        ClockShapeOptions {
66            clock_movement: ClockMovement::Sweeping,
67            show_hand_radii: true,
68            viewbox_width: 100,
69            viewbox_height: 100,
70        }
71    }
72}
73
74
75#[derive(Debug, Clone, Copy)]
76/// Clock layout information
77pub struct ClockPosition {
78    /// Position of center of the clock
79    pub center: Coordinate,
80    /// Radius of clock face
81    pub radius: f64,
82}
83
84#[derive(Debug, Clone, Copy)]
85/// Nature of clock hand movement
86pub enum ClockMovement {
87    /// Clock hands "tick", only pointing exactly to the tick marks of the clock
88    Ticking,
89    /// Clock hands "sweep", accounting for the positions of lower hands
90    Sweeping,
91}
92
93
94impl ClockShape {
95
96    /// Constructor
97    ///
98    /// # Errors
99    /// Error if integer conversion fails
100    pub fn new(
101        adic_data: &impl AdicInteger,
102        num_digits: usize,
103        options: ClockShapeOptions,
104    ) -> Result<Self, AdicShapeError> {
105
106        let p = adic_data.p();
107        let mut hands = Vec::with_capacity(num_digits);
108        let mut offset = 0.0;
109        for (i, d) in adic_data.digits().copied().chain(repeat(0)).take(num_digits).enumerate() {
110            hands.push(ClockHand{
111                order: i.try_into()?,
112                tick: d,
113                offset,
114            });
115            offset = match options.clock_movement {
116                ClockMovement::Ticking => 0.0,
117                ClockMovement::Sweeping => {
118                    (f64::from(d) + offset) / f64::from(p)
119                }
120            };
121        }
122
123        let width = f64::from(options.viewbox_width);
124        let height = f64::from(options.viewbox_height);
125        Ok(Self {
126            p,
127            options,
128            position: ClockPosition {
129                center: (0.5*width, 0.5*height),
130                radius: 0.4*width,
131            },
132            clock_hands: hands,
133        })
134
135    }
136
137    /// Number of ticks on the clock
138    pub fn p(&self) -> u32 {
139        self.p
140    }
141
142    /// Centerpoint for the clock
143    pub fn center(&self) -> (f64, f64) {
144        self.position.center
145    }
146
147    /// Clock hands
148    pub fn hands(&self) -> &Vec<ClockHand> {
149        &self.clock_hands
150    }
151
152    /// Number of clock hands in the shape
153    pub fn num_hands(&self) -> usize {
154        self.hands().len()
155    }
156
157    /// Calculate positions of the heads of the clock hands
158    pub fn hand_positions(&self) -> Vec<ClockHandPosition> {
159        self.clock_hands.iter().map(|hand| self.calc_hand_position(hand)).collect()
160    }
161
162    /// Position of the tick along the radius of the clock face
163    pub fn tick_positions(&self) -> Vec<Coordinate> {
164        (0..self.p).map(|tick| {
165            let tick_unit_coord = unit_coord(self.p, f64::from(tick));
166            let x = self.position.center.0 + self.position.radius * tick_unit_coord.0;
167            let y = self.position.center.1 + self.position.radius * tick_unit_coord.1;
168            (x, y)
169        }).collect()
170    }
171
172    fn calc_hand_position(
173        &self,
174        hand: &ClockHand,
175    ) -> ClockHandPosition {
176
177        // TODO: Move sophisticated clock head radius strategy of some sort
178        let frac_radius = f64::from(hand.order + 1) / f64::from(u32::try_from(self.num_hands() + 1).unwrap());
179        let radius = frac_radius * self.position.radius;
180
181        let tick_amount = f64::from(hand.tick) + hand.offset;
182        let unit = unit_coord(self.p, tick_amount);
183        let x = self.position.center.0 + radius * unit.0;
184        let y = self.position.center.1 + radius * unit.1;
185
186        ClockHandPosition {
187            radius,
188            head_position: (x, y),
189        }
190
191    }
192
193    fn clock_face_instructions(&self) -> impl Iterator<Item=AdicEl> {
194
195        let (cx, cy) = self.position.center;
196        let face_radius = self.position.radius;
197
198        let mut face_views = vec![
199            AdicEl::Circle(CircleEl{
200                class: Some("clock-circle".to_string()),
201                cx, cy, r: face_radius,
202            })
203        ];
204
205        if self.options.show_hand_radii {
206            self.hand_positions().iter().map(|pos| pos.radius).for_each(|r| {
207                face_views.push(
208                    AdicEl::Circle(CircleEl{
209                        class: Some("clock-sub-circle".to_string()),
210                        cx, cy, r,
211                    })
212                );
213            });
214        }
215
216        face_views.into_iter()
217
218    }
219
220    fn clock_hand_instructions(&self) -> impl Iterator<Item=AdicEl> {
221
222        // let path = svg_el::Path::new()
223        //     .set("fill", "none")
224        //     .set("stroke", "black")
225        //     .set("stroke-width", 3)
226        //     .set("d", data);
227
228        let (cx, cy) = self.position.center;
229        self.hand_positions().into_iter().flat_map(move |hand_pos| {
230            let target = hand_pos.head_position;
231            let hand_data = vec![
232                PathDInstruction::Move((cx, cy)),
233                PathDInstruction::Line(target),
234            ];
235            [
236                AdicEl::Path(PathEl{
237                    class: Some("clock-hand-path".to_string()),
238                    d: hand_data,
239                }),
240                AdicEl::Circle(CircleEl{
241                    class: Some("clock-head-circle".to_string()),
242                    cx: target.0, cy: target.1, r: 0.5,
243                })
244            ]
245        })
246
247    }
248
249    fn clock_mark_instructions(&self) -> impl Iterator<Item=AdicEl> {
250
251        let (cx, cy) = self.position.center;
252        self.tick_positions().into_iter().map(move |tick_pos| {
253            let source = (cx + (tick_pos.0 - cx) * 0.95, cy + (tick_pos.1 - cy) * 0.95);
254            let target = tick_pos;
255            let mark_instructions = vec![
256                PathDInstruction::Move(source),
257                PathDInstruction::Line(target),
258            ];
259            AdicEl::Path(PathEl{
260                class: Some("clock-hand-path".to_string()),
261                d: mark_instructions
262            })
263        })
264
265    }
266
267    fn clock_label_instructions(&self) -> impl Iterator<Item=AdicEl> {
268
269        let label_font_size = 4.;
270        // Font is in pt and non-font is not, so possibly include a magic number multiplier
271        let magic_font_multiplier = 1.;
272        let label_size = label_font_size * magic_font_multiplier;
273        let label_style = format!("position: fixed; font-size: {label_size}pt;");
274
275        let (cx, cy) = self.position.center;
276        self.tick_positions().into_iter().enumerate().map(move |(tick, tick_pos)| {
277            let adjusted = (
278                (tick_pos.0 - cx) * 0.1,
279                (tick_pos.1 - cy) * 0.1
280            );
281            // let adjusted = (0., 0.);
282            AdicEl::Text(TextEl{
283                content: tick.to_string(),
284                class: Some("tick-label".to_string()),
285                style: Some(label_style.clone()),
286                x: tick_pos.0, y: tick_pos.1,
287                dx: adjusted.0, dy: adjusted.1,
288            })
289        })
290
291    }
292
293}
294
295impl DisplayShape for ClockShape {
296
297    /// Internal SVG elements generated from this shape
298    fn adic_els(&self) -> impl Iterator<Item=AdicEl> {
299
300        // Draw the clock
301        let clock_face_circle = self.clock_face_instructions();
302        let clock_hand_paths = self.clock_hand_instructions();
303        let clock_marks = self.clock_mark_instructions();
304        let clock_labels = self.clock_label_instructions();
305
306        clock_face_circle
307            .chain(clock_hand_paths)
308            .chain(clock_marks)
309            .chain(clock_labels)
310
311    }
312
313    fn default_class(&self) -> String {
314        "adic-clock".to_string()
315    }
316
317    fn viewbox_width(&self) -> u32 {
318        self.options.viewbox_width
319    }
320    fn viewbox_height(&self) -> u32 {
321        self.options.viewbox_height
322    }
323}
324
325
326#[derive(Debug, Clone)]
327/// Data for each clock hand node
328pub struct ClockHand {
329    /// Clock hand order, e.g. second hand -> 0, minute hand -> 1
330    pub order: i32,
331    /// Tick the clock hand is indicating
332    pub tick: u32,
333    /// Offset from the tick mark (0 <= offset < 1)
334    pub offset: f64,
335}
336
337#[derive(Debug, Clone)]
338/// Data for each clock hand edge
339pub struct ClockHandPosition {
340    /// Radius from center to clock head
341    pub radius: f64,
342    /// Position of clock head
343    pub head_position: Coordinate,
344}
345
346
347
348fn unit_coord(p: u32, tick_amount: f64) -> Coordinate {
349    let arc_fraction = tick_amount / f64::from(p);
350    let x = (TAU * arc_fraction).sin();
351    let y = - (TAU * arc_fraction).cos();
352    (x, y)
353}
354
355
356
357#[cfg(test)]
358mod test {
359    use adic::uadic;
360    use super::{ClockMovement, ClockShape, ClockShapeOptions};
361
362    use super::super::test_util::assert_diff_lt;
363
364    #[test]
365    fn correct_numbers() {
366
367        let adic_data = uadic!(5, [3, 2, 4, 1, 4, 1, 2]);
368        let shape_options = ClockShapeOptions::default();
369
370        let clock_shape = ClockShape::new(&adic_data, 6, shape_options).unwrap();
371
372        assert_eq!(5, clock_shape.p);
373        assert_eq!(6, clock_shape.num_hands());
374
375        let hands = clock_shape.clock_hands;
376        assert_eq!(0, hands[0].order);
377        assert_eq!(3, hands[0].tick);
378        assert_eq!(1, hands[1].order);
379        assert_eq!(2, hands[1].tick);
380        assert_eq!(2, hands[2].order);
381        assert_eq!(4, hands[2].tick);
382        assert_eq!(3, hands[3].order);
383        assert_eq!(1, hands[3].tick);
384        assert_eq!(4, hands[4].order);
385        assert_eq!(4, hands[4].tick);
386        assert_eq!(5, hands[5].order);
387        assert_eq!(1, hands[5].tick);
388
389    }
390
391    #[test]
392    fn correct_bounds() {
393
394        let adic_data = uadic!(5, [3, 2, 4, 1, 4, 1, 2]);
395        let shape_options = ClockShapeOptions {
396            clock_movement: ClockMovement::Ticking,
397            ..Default::default()
398        };
399        let clock_shape = ClockShape::new(&adic_data, 6, shape_options).unwrap();
400        let hand_pos = clock_shape.hand_positions();
401        assert!(0. <= hand_pos.iter().map(|hand| hand.head_position.0).min_by(f64::total_cmp).unwrap());
402        assert!(100. >= hand_pos.iter().map(|hand| hand.head_position.0).max_by(f64::total_cmp).unwrap());
403        assert!(0. <= hand_pos.iter().map(|hand| hand.head_position.1).min_by(f64::total_cmp).unwrap());
404        assert!(100. >= hand_pos.iter().map(|hand| hand.head_position.1).max_by(f64::total_cmp).unwrap());
405        assert!(0. <= hand_pos.iter().map(|hand| hand.radius).min_by(f64::total_cmp).unwrap());
406        assert!(50. >= hand_pos.iter().map(|hand| hand.radius).max_by(f64::total_cmp).unwrap());
407
408    }
409
410    #[test]
411    fn correct_offsets() {
412
413        let adic_data = uadic!(5, [3, 2, 4, 1, 4, 1, 2]);
414        let shape_options = ClockShapeOptions {
415            clock_movement: ClockMovement::Ticking,
416            ..Default::default()
417        };
418        let clock_shape = ClockShape::new(&adic_data, 6, shape_options).unwrap();
419
420        for hand in clock_shape.clock_hands {
421            assert_diff_lt!(0., hand.offset, 0.1);
422        }
423
424        let shape_options = ClockShapeOptions {
425            clock_movement: ClockMovement::Sweeping,
426            ..Default::default()
427        };
428        let clock_shape = ClockShape::new(&adic_data, 6, shape_options).unwrap();
429
430        let hands = clock_shape.clock_hands;
431        assert_diff_lt!(0., hands[0].offset, 0.1);
432        assert_diff_lt!(0.6, hands[1].offset, 0.1);
433        assert_diff_lt!(0.52, hands[2].offset, 0.1);
434        assert_diff_lt!(0.90, hands[3].offset, 0.1);
435        assert_diff_lt!(0.38, hands[4].offset, 0.1);
436        assert_diff_lt!(0.88, hands[5].offset, 0.1);
437
438
439    }
440
441}