Skip to main content

blockworx_editor/render/
pin.rs

1use blockworx_doc::id::PinId;
2use blockworx_geom::{Align, Pos2, Rect, Vec2, pos2, vec2};
3
4use crate::{
5    grid::{BLOCK_STROKE_WIDTH, GRID_SIZE, pin_offset_y},
6    shape::pin::{Pin, PinDir, PinSide, slot},
7    theme::{Role, RoleStroke, Style, accent_role},
8};
9use blockworx_paint::Renderer;
10
11use super::{draw_pin_labels, pin_tag_location, pin_text_location};
12
13/// Length of a pin/port arrowhead along the stub, in world units.
14const PIN_ARROW_LEN: f32 = GRID_SIZE * 0.4;
15/// Half-width of a pin/port arrowhead base, in world units.
16const PIN_ARROW_HALF_W: f32 = GRID_SIZE * 0.22;
17/// Half the block/port outline stroke width: a pin stub's block-side endpoint
18/// sits this far *outside* the bbox edge so it butts against the outline's outer
19/// face. The outline is centered on the edge (`StrokeKind::Middle`), so its
20/// outer surface lies half a stroke-width out; stopping the stub there makes a
21/// clean butt-joint instead of letting it run across the outline.
22const FRAME_HALF_STROKE: f32 = BLOCK_STROKE_WIDTH / 2.0;
23
24/// Draw a pin/port stub from `edge` (the outline's outer face — see
25/// `pin_stub_endpoints`) out to `tip`, with an arrowhead flush against the
26/// shape so input and output pins read symmetrically:
27/// - `Input`: apex on the edge, pointing into the shape.
28/// - `Output`: base on the edge, pointing away (apex one length out).
29/// - `InOut`: a plain line, no arrowhead.
30///
31/// The stub starts exactly at `edge`, so it butts against the outline instead of
32/// crossing into it; its bare remainder reaches `tip`, where routes connect.
33pub fn draw_pin_stub(
34    edge: Pos2,
35    tip: Pos2,
36    kind: PinDir,
37    stroke: impl Into<RoleStroke>,
38    painter: &Style<'_, impl Renderer>,
39) {
40    let stroke = stroke.into();
41    let outward = (tip - edge).normalized();
42    painter.line_segment([edge, tip], stroke);
43    match kind {
44        PinDir::InOut => {}
45        // Apex on the edge, pointing into the shape.
46        PinDir::Input => draw_arrowhead(edge, -outward, stroke.role, painter),
47        // Base on the edge, pointing out; apex one length along the stub.
48        PinDir::Output => draw_arrowhead(
49            edge + outward * PIN_ARROW_LEN,
50            outward,
51            stroke.role,
52            painter,
53        ),
54    }
55}
56
57/// Draw a filled triangular arrowhead with its point at `head`, pointing in
58/// `dir`.
59fn draw_arrowhead(head: Pos2, dir: Vec2, color: Role, painter: &Style<'_, impl Renderer>) {
60    let dir = dir.normalized();
61    let perp = vec2(-dir.y, dir.x);
62    let base = head - dir * PIN_ARROW_LEN;
63    let p1 = base + perp * PIN_ARROW_HALF_W;
64    let p2 = base - perp * PIN_ARROW_HALF_W;
65    painter.add_convex_polygon(vec![head, p1, p2], color, RoleStroke::NONE);
66}
67
68/// One pin's per-draw adjustments: the drag shift, a forced side, and
69/// the resolved stub accent (derived from route roles, never stored on
70/// the pin).
71#[derive(Clone, Copy, Default)]
72pub struct PinPaint {
73    pub delta_y: f32,
74    pub side: Option<PinSide>,
75    pub accent: Option<u8>,
76}
77
78/// Draw a pin: its stub (in `paint.accent`, else `PinStem`), its `name`,
79/// and its `tag`.
80pub fn draw_pin(bbox: Rect, pin: &Pin, paint: PinPaint, painter: &mut Style<'_, impl Renderer>) {
81    let stub_stroke = (1.7, accent_role(paint.accent).unwrap_or(Role::PinStem));
82    let tag_color = Role::PinTag;
83    let side = paint.side.unwrap_or(slot(pin).side);
84    let delta_y = paint.delta_y;
85    let (text_pos, _) = pin_text_location(bbox, pin, side, delta_y);
86    let (stub_a, stub_b) = pin_stub_endpoints(bbox, pin, side, delta_y);
87    // Rendered as a pin: the stored sense is used directly.
88    draw_pin_stub(stub_a, stub_b, pin.dir, stub_stroke, painter);
89    let h_align = match side {
90        PinSide::East => Align::Max,
91        PinSide::West => Align::Min,
92    };
93    // Empty labels draw nothing. A hidden (or empty) tag also draws nothing here;
94    // when the pin's block is selected the placeholder overlay shows empty
95    // name/type slots faint instead.
96    draw_pin_labels(text_pos, h_align, &pin.name, &pin.type_name, painter);
97    if !pin.tag_hidden && !pin.tag.is_empty() {
98        let (tag_pos, tag_align) = pin_tag_location(bbox.left(), bbox.right(), side, stub_a.y);
99        painter.text(
100            tag_pos,
101            tag_align,
102            pin.tag.clone(),
103            &painter.theme().tag_font,
104            tag_color,
105        );
106    }
107}
108
109/// Draw the faint "Add Name"/"Add Type"/"+tag" prompts for one block pin's empty
110/// label slots — used by the selected-block render so empty fields are
111/// discoverable. The "+tag" prompt is only drawn for a shown (not hidden) tag.
112pub fn draw_pin_placeholders(bbox: Rect, pin: &Pin, painter: &mut Style<'_, impl Renderer>) {
113    let slot = slot(pin);
114    let (text_pos, _) = pin_text_location(bbox, pin, slot.side, 0.0);
115    let h_align = match slot.side {
116        PinSide::East => Align::Max,
117        PinSide::West => Align::Min,
118    };
119    super::draw_label_placeholders(text_pos, h_align, &pin.name, &pin.type_name, painter);
120    if !pin.tag_hidden && pin.tag.is_empty() {
121        let line_y = pin_offset_y(bbox.top(), slot.offset);
122        super::draw_tag_placeholder(bbox.left(), bbox.right(), slot.side, line_y, painter);
123    }
124}
125
126/// The pin stub's two endpoints: `a` on the outline's outer face ([`FRAME_HALF_STROKE`]
127/// outside the bbox edge, so the stub butts against it), `b` one cell outward.
128/// `side` is the (possibly overridden) side and `delta_y` shifts it during a
129/// drag preview.
130fn pin_stub_endpoints(bbox: Rect, pin: &Pin, side: PinSide, delta_y: f32) -> (Pos2, Pos2) {
131    let y = pin_offset_y(bbox.top(), slot(pin).offset) + delta_y;
132    match side {
133        PinSide::East => (
134            pos2(bbox.right() + FRAME_HALF_STROKE, y),
135            pos2(bbox.right() + GRID_SIZE, y),
136        ),
137        PinSide::West => (
138            pos2(bbox.left() - FRAME_HALF_STROKE, y),
139            pos2(bbox.left() - GRID_SIZE, y),
140        ),
141    }
142}
143
144pub fn render_pins_with_box<'a>(
145    iter: impl Iterator<Item = (PinId, &'a Pin)>,
146    bbox: Rect,
147    accents: crate::presentation::ShapeAccents<'_>,
148    painter: &mut Style<'_, impl Renderer>,
149) {
150    for (id, pin) in iter {
151        let paint = PinPaint {
152            accent: accents.pin(id),
153            ..PinPaint::default()
154        };
155        draw_pin(bbox, pin, paint, painter);
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::shape::block::tests::test_pin as pin;
163
164    #[test]
165    fn stub_butts_against_the_outline_outer_face() {
166        // The block-side endpoint sits half the outline stroke-width *outside* the
167        // bbox edge (a butt-joint), never on or inside it. For a right edge at 180
168        // that is 180.5; for a left edge at 60 it is 59.5. The tip is one cell out.
169        let bbox = Rect::from_min_max(pos2(60.0, 30.0), pos2(180.0, 150.0));
170
171        let (east_a, east_b) = pin_stub_endpoints(bbox, &pin(PinSide::East, 0), PinSide::East, 0.0);
172        assert_eq!(east_a.x, bbox.right() + FRAME_HALF_STROKE);
173        assert_eq!(east_b.x, bbox.right() + GRID_SIZE);
174        assert!(east_a.x > bbox.right(), "stub starts outside the edge");
175
176        let (west_a, west_b) = pin_stub_endpoints(bbox, &pin(PinSide::West, 0), PinSide::West, 0.0);
177        assert_eq!(west_a.x, bbox.left() - FRAME_HALF_STROKE);
178        assert_eq!(west_b.x, bbox.left() - GRID_SIZE);
179        assert!(west_a.x < bbox.left(), "stub starts outside the edge");
180    }
181}