1use blockworx_doc::values::LabelSide;
6use blockworx_geom::{Align, Align2, Pos2, Rect, pos2, vec2};
7
8use crate::{
9 grid::{GRID_SIZE, PORT_TEXT_SIZE, TAG_SHIM, pin_offset_y},
10 shape::{
11 ShapeLabel,
12 pin::{Pin, PinSide, slot},
13 },
14};
15
16const PIN_TAG_RISE: f32 = PORT_TEXT_SIZE * 0.7;
19
20pub fn port_text_center(bbox: Rect, side: PinSide) -> Pos2 {
21 let y = bbox.center().y - GRID_SIZE / 2.0;
22 match side {
23 PinSide::East => pos2(bbox.center().x - GRID_SIZE / 2.0, y),
24 PinSide::West => pos2(bbox.center().x + GRID_SIZE / 2.0, y),
25 }
26}
27
28pub fn pin_text_location(bbox: Rect, pin: &Pin, side: PinSide, offset: f32) -> (Pos2, Align2) {
29 let y_coord = pin_offset_y(bbox.top(), slot(pin).offset) + offset;
30 match side {
31 PinSide::East => (
32 pos2(bbox.right() - GRID_SIZE / 2.0, y_coord),
33 Align2::RIGHT_CENTER,
34 ),
35 PinSide::West => (
36 pos2(bbox.left() + GRID_SIZE / 2.0, y_coord),
37 Align2::LEFT_CENTER,
38 ),
39 }
40}
41
42pub fn pin_tag_location(left: f32, right: f32, side: PinSide, line_y: f32) -> (Pos2, Align2) {
48 let y = line_y - PIN_TAG_RISE;
49 match side {
50 PinSide::East => (pos2(right + TAG_SHIM, y), Align2::LEFT_CENTER),
51 PinSide::West => (pos2(left - TAG_SHIM, y), Align2::RIGHT_CENTER),
52 }
53}
54
55fn title_anchor(bbox: Rect, side: LabelSide, offset: f32) -> (Pos2, Align2) {
56 match side {
57 LabelSide::Bottom => (bbox.center_bottom() + vec2(offset, 0.0), Align2::CENTER_TOP),
58 LabelSide::Top => (bbox.center_top() + vec2(offset, 0.0), Align2::CENTER_BOTTOM),
59 LabelSide::Center => (bbox.center() + vec2(offset, 0.0), Align2::CENTER_CENTER),
60 }
61}
62
63pub fn block_title_position(bbox: Rect, title: &ShapeLabel<'_>) -> (Pos2, Align2) {
64 title_anchor(bbox, title.side, title.offset)
65}
66
67pub fn clamped_block_title_position(
71 bbox: Rect,
72 title: &ShapeLabel<'_>,
73 text_width: f32,
74) -> (Pos2, Align2) {
75 let offset = clamp_label_offset(bbox, text_width, title.offset, Align::Center);
76 title_anchor(bbox, title.side, offset)
77}
78
79pub fn clamp_label_offset(bbox: Rect, text_width: f32, offset: f32, h_align: Align) -> f32 {
85 let slack = (bbox.width() - text_width).max(0.0);
86 let (lo, hi) = match h_align {
87 Align::Min => (0.0, slack),
88 Align::Center => (-slack / 2.0, slack / 2.0),
89 Align::Max => (-slack, 0.0),
90 };
91 offset.clamp(lo, hi)
92}
93
94pub fn label_side_for_y(bbox: Rect, y: f32) -> LabelSide {
98 let third = bbox.height() / 3.0;
99 if y < bbox.top() + third {
100 LabelSide::Top
101 } else if y > bbox.top() + 2.0 * third {
102 LabelSide::Bottom
103 } else {
104 LabelSide::Center
105 }
106}
107
108fn type_anchor(bbox: Rect, side: LabelSide, offset: f32) -> (Pos2, Align2) {
113 match side {
114 LabelSide::Top => (bbox.left_top() + vec2(offset, 0.0), Align2::LEFT_BOTTOM),
115 LabelSide::Center => (bbox.left_center() + vec2(offset, 0.0), Align2::LEFT_CENTER),
116 LabelSide::Bottom => (bbox.left_bottom() + vec2(offset, 0.0), Align2::LEFT_TOP),
117 }
118}
119
120pub fn block_type_position(bbox: Rect, type_label: &ShapeLabel<'_>) -> (Pos2, Align2) {
121 type_anchor(bbox, type_label.side, type_label.offset)
122}
123
124pub fn clamped_block_type_position(
128 bbox: Rect,
129 type_label: &ShapeLabel<'_>,
130 text_width: f32,
131) -> (Pos2, Align2) {
132 let offset = clamp_label_offset(bbox, text_width, type_label.offset, Align::Min);
133 type_anchor(bbox, type_label.side, offset)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn clamp_label_offset_keeps_each_anchor_within_the_block_width() {
142 let bbox = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 40.0));
143 assert_eq!(clamp_label_offset(bbox, 40.0, 100.0, Align::Center), 30.0);
145 assert_eq!(clamp_label_offset(bbox, 40.0, -100.0, Align::Center), -30.0);
146 assert_eq!(clamp_label_offset(bbox, 40.0, 20.0, Align::Center), 20.0);
147 assert_eq!(clamp_label_offset(bbox, 40.0, -100.0, Align::Min), 0.0);
149 assert_eq!(clamp_label_offset(bbox, 40.0, 100.0, Align::Min), 60.0);
150 assert_eq!(clamp_label_offset(bbox, 40.0, 100.0, Align::Max), 0.0);
152 assert_eq!(clamp_label_offset(bbox, 40.0, -100.0, Align::Max), -60.0);
153 assert_eq!(clamp_label_offset(bbox, 140.0, 50.0, Align::Center), 0.0);
155 assert_eq!(clamp_label_offset(bbox, 140.0, 50.0, Align::Min), 0.0);
156 }
157
158 #[test]
159 fn block_type_position_sits_at_the_upper_left_by_default() {
160 let bbox = Rect::from_min_max(pos2(10.0, 20.0), pos2(110.0, 60.0));
161 let (pos, align) = block_type_position(bbox, &ShapeLabel::default());
163 assert_eq!(pos, bbox.left_top());
166 assert_eq!(align, Align2::LEFT_BOTTOM);
167 }
168
169 #[test]
170 fn bottom_side_labels_hang_below_the_edge_growing_downward() {
171 let bbox = Rect::from_min_max(pos2(10.0, 20.0), pos2(110.0, 60.0));
175 let bottom = |side| ShapeLabel {
176 side,
177 ..Default::default()
178 };
179
180 let (pos, align) = block_title_position(bbox, &bottom(LabelSide::Bottom));
181 assert_eq!(pos, bbox.center_bottom());
182 assert_eq!(align, Align2::CENTER_TOP);
183
184 let (pos, align) = block_type_position(bbox, &bottom(LabelSide::Bottom));
185 assert_eq!(pos, bbox.left_bottom());
186 assert_eq!(align, Align2::LEFT_TOP);
187 }
188
189 fn label(side: LabelSide, offset: f32) -> ShapeLabel<'static> {
190 ShapeLabel {
191 side,
192 offset,
193 ..Default::default()
194 }
195 }
196
197 fn centered_span(x: f32, text_width: f32) -> (f32, f32) {
199 (x - text_width / 2.0, x + text_width / 2.0)
200 }
201
202 fn left_span(x: f32, text_width: f32) -> (f32, f32) {
204 (x, x + text_width)
205 }
206
207 #[test]
208 fn resizing_narrower_reclamps_a_title_slid_to_the_wide_edge_back_inside() {
209 let text_width = 40.0;
210 let wide = Rect::from_min_max(pos2(0.0, 0.0), pos2(200.0, 40.0));
211 let narrow = Rect::from_min_max(pos2(0.0, 0.0), pos2(80.0, 40.0));
212
213 let wide_offset = clamp_label_offset(wide, text_width, 1_000.0, Align::Center);
216 let title = label(LabelSide::Center, wide_offset);
217
218 let (raw_pos, _) = block_title_position(narrow, &title);
219 let (clamped_pos, _) = clamped_block_title_position(narrow, &title, text_width);
220 assert!(
221 clamped_pos.x < raw_pos.x,
222 "re-clamp must move the title strictly inward",
223 );
224
225 let (lo, hi) = centered_span(clamped_pos.x, text_width);
226 assert!(lo >= narrow.left() && hi <= narrow.right());
227 }
228
229 #[test]
230 fn resizing_narrower_reclamps_a_type_slid_to_the_wide_edge_back_inside() {
231 let text_width = 40.0;
232 let wide = Rect::from_min_max(pos2(0.0, 0.0), pos2(200.0, 40.0));
233 let narrow = Rect::from_min_max(pos2(0.0, 0.0), pos2(80.0, 40.0));
234
235 let wide_offset = clamp_label_offset(wide, text_width, 1_000.0, Align::Min);
236 let type_label = label(LabelSide::Top, wide_offset);
237
238 let (raw_pos, _) = block_type_position(narrow, &type_label);
239 let (clamped_pos, _) = clamped_block_type_position(narrow, &type_label, text_width);
240 assert!(
241 clamped_pos.x < raw_pos.x,
242 "re-clamp must move the type label strictly inward",
243 );
244
245 let (lo, hi) = left_span(clamped_pos.x, text_width);
246 assert!(lo >= narrow.left() && hi <= narrow.right());
247 }
248
249 #[test]
250 fn a_label_wider_than_the_block_pins_flush_for_every_side_and_anchor() {
251 let narrow = Rect::from_min_max(pos2(10.0, 0.0), pos2(60.0, 40.0));
252 let text_width = 200.0; for side in [LabelSide::Top, LabelSide::Center, LabelSide::Bottom] {
255 let title = label(side, 500.0);
257 let (pos, _) = clamped_block_title_position(narrow, &title, text_width);
258 assert_eq!(pos.x, narrow.center().x);
259
260 let type_label = label(side, 500.0);
262 let (pos, _) = clamped_block_type_position(narrow, &type_label, text_width);
263 assert_eq!(pos.x, narrow.left());
264 }
265 }
266}