Oregami
Repositories/oxedyne/fe2o3

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
8use crate::ir::Sp;
9
10use oxedyne_fe2o3_core::prelude::*;
11use 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)]
20pub 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
27impl 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)]
40pub 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)]
51pub 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
58impl 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.
151pub 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}