oxedyne/fe2o3/fe2o3_austenite/src/diagram/shape.rs
5.4 KiB, 7 runs
created by r1870400018:36167, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Node shapes: the outline a box draws, and the named points an edge joins. |
| 2 | //! |
| 3 | //! A shape sizes itself to its label plus padding, draws its outline as one closed |
| 4 | //! `fe2o3_graphics` path in the figure's own frame (y down, in points), and exposes the five ports. |
| 5 | //! The ports are the bounding box's four side midpoints and its centre, which for a diamond are its |
| 6 | //! four vertices and centre, so one port rule serves every shape. |
| 7 | |
| 8 | use crate::ir::Sp; |
| 9 | |
| 10 | use oxedyne_fe2o3_core::prelude::*; |
| 11 | use oxedyne_fe2o3_graphics::path::{ |
| 12 | Bounds, |
| 13 | Path, |
| 14 | PathBuilder, |
| 15 | Pt, |
| 16 | }; |
| 17 | |
| 18 | /// A placed node's box in the figure frame: its top-left corner and its extent, all scaled points. |
| 19 | #[derive(Clone, Copy, Debug)] |
| 20 | pub struct Rect { |
| 21 | pub x: Sp, // left edge |
| 22 | pub y: Sp, // top edge, the frame being y down |
| 23 | pub w: Sp, |
| 24 | pub h: Sp, |
| 25 | } |
| 26 | |
| 27 | impl Rect { |
| 28 | pub fn new(x: Sp, y: Sp, w: Sp, h: Sp) -> Self { |
| 29 | Self { x, y, w, h } |
| 30 | } |
| 31 | |
| 32 | pub fn centre_x(&self) -> Sp { self.x + Sp(self.w.raw() / 2) } |
| 33 | pub fn centre_y(&self) -> Sp { self.y + Sp(self.h.raw() / 2) } |
| 34 | pub fn right(&self) -> Sp { self.x + self.w } |
| 35 | pub fn bottom(&self) -> Sp { self.y + self.h } |
| 36 | } |
| 37 | |
| 38 | /// A named point on a node's box, where an edge attaches. |
| 39 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 40 | pub enum Port { |
| 41 | North, |
| 42 | South, |
| 43 | East, |
| 44 | West, |
| 45 | Centre, |
| 46 | } |
| 47 | |
| 48 | /// The outline a node draws. An enum, not a trait object, so a new shape is a new arm the sizing and |
| 49 | /// the drawing must both answer -- the house preference for concrete types. |
| 50 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 51 | pub enum Shape { |
| 52 | Box, // a plain rectangle, the default process step |
| 53 | Stadium, // a rectangle with fully rounded ends, a start or terminal |
| 54 | Diamond, // a rhombus on the box's side midpoints, a decision |
| 55 | Hexagon, // a horizontal hexagon with slanted ends, a preparation or setup step |
| 56 | } |
| 57 | |
| 58 | impl Shape { |
| 59 | /// The box that holds a label of the given width and vertical extent, once padded. A stadium adds |
| 60 | /// its two semicircular caps to the width so the label clears the curve; a diamond is grown so the |
| 61 | /// label's rectangle sits inside the rhombus rather than poking through its sloped sides. |
| 62 | /// |
| 63 | /// # Arguments |
| 64 | /// `label_ext` is the label's height plus depth, its full vertical extent; the pads are the gap |
| 65 | /// wanted on each side, in scaled points. |
| 66 | pub fn size_for_label( |
| 67 | &self, |
| 68 | label_w: Sp, |
| 69 | label_ext: Sp, |
| 70 | pad_x: Sp, |
| 71 | pad_y: Sp, |
| 72 | ) |
| 73 | -> (Sp, Sp) |
| 74 | { |
| 75 | match self { |
| 76 | Shape::Box => ( |
| 77 | label_w + pad_x + pad_x, |
| 78 | label_ext + pad_y + pad_y, |
| 79 | ), |
| 80 | Shape::Stadium => ( |
| 81 | // The caps eat about half the height at each end, so a full extent of width is added. |
| 82 | label_w + label_ext + pad_x + pad_x, |
| 83 | label_ext + pad_y + pad_y, |
| 84 | ), |
| 85 | Shape::Diamond => ( |
| 86 | // A rhombus twice the label's size holds the label rectangle with room to spare, since |
| 87 | // the rectangle's corner then sits just inside the sloped side. |
| 88 | Sp(label_w.raw() * 2) + Sp(pad_x.raw() * 4), |
| 89 | Sp(label_ext.raw() * 2) + Sp(pad_y.raw() * 4), |
| 90 | ), |
| 91 | Shape::Hexagon => ( |
| 92 | // The slanted caps eat about a cap width at each end, so a full extent of width is added |
| 93 | // so the label's rectangle clears the slopes, as a stadium adds its caps. |
| 94 | label_w + label_ext + pad_x + pad_x, |
| 95 | label_ext + pad_y + pad_y, |
| 96 | ), |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// The outline as one closed path, in the figure frame (y down, points). |
| 101 | pub fn outline(&self, r: &Rect) -> Outcome<Path> { |
| 102 | let x0 = r.x.to_pt() as f32; |
| 103 | let y0 = r.y.to_pt() as f32; |
| 104 | let x1 = r.right().to_pt() as f32; |
| 105 | let y1 = r.bottom().to_pt() as f32; |
| 106 | match self { |
| 107 | Shape::Box => Path::rect(Bounds::new(x0, y0, x1, y1)), |
| 108 | Shape::Stadium => { |
| 109 | // A radius of half the height rounds the ends to true semicircles; round_rect clamps to |
| 110 | // half the shorter side, so a tall narrow box degrades to its inscribed stadium. |
| 111 | let radius = (r.h.to_pt() as f32) * 0.5; |
| 112 | Path::round_rect(Bounds::new(x0, y0, x1, y1), radius) |
| 113 | }, |
| 114 | Shape::Diamond => { |
| 115 | let cx = r.centre_x().to_pt() as f32; |
| 116 | let cy = r.centre_y().to_pt() as f32; |
| 117 | let mut pb = PathBuilder::new(); |
| 118 | pb.move_to(Pt::new(cx, y0)); // north vertex |
| 119 | pb.line_to(Pt::new(x1, cy)); // east |
| 120 | pb.line_to(Pt::new(cx, y1)); // south |
| 121 | pb.line_to(Pt::new(x0, cy)); // west |
| 122 | pb.close(); |
| 123 | pb.finish() |
| 124 | }, |
| 125 | Shape::Hexagon => { |
| 126 | let cy = r.centre_y().to_pt() as f32; |
| 127 | // The slant eats two-fifths of the height at each end, a gentle taper matching the |
| 128 | // preparation hexagon these flowcharts draw. |
| 129 | let cap = (r.h.to_pt() as f32) * 0.4; |
| 130 | let mut pb = PathBuilder::new(); |
| 131 | pb.move_to(Pt::new(x0, cy)); // west vertex |
| 132 | pb.line_to(Pt::new(x0 + cap, y0)); // top-left shoulder |
| 133 | pb.line_to(Pt::new(x1 - cap, y0)); // top-right shoulder |
| 134 | pb.line_to(Pt::new(x1, cy)); // east vertex |
| 135 | pb.line_to(Pt::new(x1 - cap, y1)); // bottom-right shoulder |
| 136 | pb.line_to(Pt::new(x0 + cap, y1)); // bottom-left shoulder |
| 137 | pb.close(); |
| 138 | pb.finish() |
| 139 | }, |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | /// A port's coordinate on the box. The rule is the bounding box's side midpoints and centre, which |
| 144 | /// coincide with a diamond's own vertices, so the shape does not enter the calculation. |
| 145 | pub fn port(&self, r: &Rect, port: Port) -> (Sp, Sp) { |
| 146 | port_of(r, port) |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /// The bounding-box port coordinate, shared by every shape. |
| 151 | pub fn port_of(r: &Rect, port: Port) -> (Sp, Sp) { |
| 152 | match port { |
| 153 | Port::North => (r.centre_x(), r.y), |
| 154 | Port::South => (r.centre_x(), r.bottom()), |
| 155 | Port::East => (r.right(), r.centre_y()), |
| 156 | Port::West => (r.x, r.centre_y()), |
| 157 | Port::Centre => (r.centre_x(), r.centre_y()), |
| 158 | } |
| 159 | } |