Skip to main content

blockworx/render/
route.rs

1use blockworx_doc::block_model::Route;
2use blockworx_geom::{Align2, Pos2, vec2};
3
4use crate::{
5    edit::{lower::accent_from_role, naming::InterfaceLock},
6    grid::{PORT_RADIUS, SHIM, px_point},
7    presentation::{LocAndDirection, RouteDirection},
8    theme::{Role, RoleStroke, Style, accent_role},
9    widget::auto_route::{RouteGeometry, Wire, text_anchors},
10};
11use blockworx_paint::Renderer;
12
13use super::{ADD_ROUTE_LABEL_PLACEHOLDER, render_path_with_chamfered_corners};
14use crate::edit::naming::Authoring;
15
16/// Whether a route render includes its labels, or only the wire (the label pass
17/// is skipped where labels are drawn separately, over every wire).
18#[derive(Clone, Copy, PartialEq, Eq)]
19pub enum LabelPass {
20    Draw,
21    Skip,
22}
23
24#[derive(Clone, Copy, PartialEq, Eq)]
25pub enum RouteRenderMode {
26    Normal,
27    Highlighted,
28    Selected {
29        /// Whether the wire's editing affordances — waypoint handles and the
30        /// text anchors a new label attaches to — come with the halo. A
31        /// read-only session shows the selection without them.
32        authoring: Authoring,
33    },
34    /// Mid-edit: the selection halo without the stored waypoint handles and
35    /// text anchors — the editing tool draws its *working* corner set instead,
36    /// which the document does not hold until the edit commits.
37    Editing,
38}
39
40fn draw_text_anchor(
41    painter: &mut Style<'_, impl Renderer>,
42    ta: Pos2,
43    fill: Role,
44    stroke: impl Into<RoleStroke>,
45) {
46    painter.add_convex_polygon(
47        [
48            ta + vec2(0.0, -PORT_RADIUS.get()),
49            ta + vec2(PORT_RADIUS.get(), 0.0),
50            ta + vec2(0.0, PORT_RADIUS.get()),
51            ta + vec2(-PORT_RADIUS.get(), 0.0),
52        ]
53        .into(),
54        fill,
55        stroke,
56    );
57}
58
59/// Resting wire width, world units.
60const ROUTE_WIDTH: f32 = 1.7;
61/// Wire width when hovered, world units.
62const ROUTE_HOVER_WIDTH: f32 = 2.3;
63/// Width of the foreground halo drawn behind a selected route, world units —
64/// wide enough to peek out either side of the resting wire on top of it.
65const ROUTE_SELECTION_HALO_WIDTH: f32 = 2.8;
66
67/// World-space anchor where a *horizontal* route label is drawn (its bottom
68/// center, nudged up off the wire). Shared with the in-place editor so it can
69/// sit exactly over the text it replaces.
70pub fn horizontal_label_anchor(pos: Pos2) -> Pos2 {
71    pos + vec2(0.0, -SHIM / 4.0)
72}
73
74/// The color `route`'s label text takes under `mode`: the wire's accent when
75/// set, else the mode's resting or highlight color.
76pub fn route_text_color(route: &Route, mode: RouteRenderMode) -> Role {
77    let accent = accent_role(accent_from_role(route.role));
78    match mode {
79        RouteRenderMode::Highlighted => accent.unwrap_or(Role::RouteHighlighted),
80        _ => accent.unwrap_or(Role::RouteNormal),
81    }
82}
83
84/// Draw one of `route`'s labels at `at`, oriented along the wire there. An
85/// unnamed route shows the faint "add name" prompt (mirroring the pin
86/// placeholder); a named one shows the name in `color`.
87pub fn render_route_label_at(
88    painter: &mut Style<'_, impl Renderer>,
89    route: &Route,
90    at: LocAndDirection,
91    color: Role,
92) {
93    let name = route.name.as_str();
94    let (text, color) = if name.is_empty() {
95        (ADD_ROUTE_LABEL_PLACEHOLDER, Role::PinLabelPlaceholder)
96    } else {
97        (name, color)
98    };
99    let route_font = &painter.theme().route_font;
100    match at.direction {
101        RouteDirection::Horizontal => {
102            painter.text(
103                horizontal_label_anchor(at.location),
104                Align2::CENTER_BOTTOM,
105                text,
106                route_font,
107                color,
108            );
109        }
110        RouteDirection::Vertical => {
111            let text_size = painter.text_size(text, route_font);
112            painter.rotated_text(
113                at.location + vec2(text_size.y + SHIM / 4.0, -text_size.x / 2.0),
114                Align2::LEFT_TOP,
115                text,
116                route_font,
117                color,
118                std::f32::consts::FRAC_PI_2,
119            );
120        }
121    }
122}
123
124/// Draw waypoint handles: a user-pinned (locked) corner prominently, a
125/// structural bend the router laid down faint and small.
126pub fn render_waypoint_handles(
127    painter: &mut Style<'_, impl Renderer>,
128    handles: impl IntoIterator<Item = (Pos2, InterfaceLock)>,
129) {
130    for (pos, lock) in handles {
131        let (radius, fill) = if lock.is_locked() {
132            (PORT_RADIUS, Role::WaypointFill)
133        } else {
134            (PORT_RADIUS * 0.6, Role::ControlHandleFill)
135        };
136        painter.circle(pos, radius, fill, (0.5, Role::ControlHandleStroke));
137    }
138}
139
140/// Draw a route's text-anchor diamonds (the spots a new label can attach).
141pub fn render_text_anchors(
142    painter: &mut Style<'_, impl Renderer>,
143    anchors: impl IntoIterator<Item = Pos2>,
144) {
145    for ta in anchors {
146        draw_text_anchor(
147            painter,
148            ta,
149            Role::WaypointFill,
150            (0.5, Role::ControlHandleStroke),
151        );
152    }
153}
154
155pub(crate) fn render_route(
156    painter: &mut Style<'_, impl Renderer>,
157    wire: &Wire<'_>,
158    geometry: &RouteGeometry,
159    mode: RouteRenderMode,
160    labels: LabelPass,
161) {
162    let route = wire.route;
163    let accent = accent_role(accent_from_role(route.role));
164    // The wire's resting color: its accent if set, else the normal route color.
165    // The accent wins over the mode color; hover/select add their own emphasis.
166    let wire_color = accent.unwrap_or(Role::RouteNormal);
167    let crossings = &geometry.crossings;
168    let points = render_path_with_chamfered_corners(&geometry.points());
169    match mode {
170        RouteRenderMode::Normal => {
171            points.render_with_hops(painter, (ROUTE_WIDTH, wire_color), crossings);
172        }
173        RouteRenderMode::Highlighted => {
174            let hl = accent.unwrap_or(Role::RouteHighlighted);
175            points.render_with_hops(painter, (ROUTE_HOVER_WIDTH, hl), crossings);
176        }
177        RouteRenderMode::Selected { .. } | RouteRenderMode::Editing => {
178            // A wider foreground halo underneath, then the resting wire on top.
179            points.render_with_hops(
180                painter,
181                (ROUTE_SELECTION_HALO_WIDTH, Role::RouteSelected),
182                crossings,
183            );
184            points.render_with_hops(painter, (ROUTE_WIDTH, wire_color), crossings);
185        }
186    }
187    if labels == LabelPass::Draw {
188        let color = route_text_color(route, mode);
189        for &(_, label) in &wire.labels {
190            let at = geometry.map_linear_distance_to_position(label);
191            render_route_label_at(painter, route, at, color);
192        }
193    }
194    if matches!(
195        mode,
196        RouteRenderMode::Selected {
197            authoring: Authoring::Offered
198        }
199    ) {
200        // Every corner is a waypoint. `Editing` deliberately skips these: the
201        // stored set is stale mid-drag, so the tool draws its working set.
202        render_waypoint_handles(
203            painter,
204            route.waypoints.iter().map(|wp| {
205                let lock = if wp.locked {
206                    InterfaceLock::Locked
207                } else {
208                    InterfaceLock::Unlocked
209                };
210                (px_point(wp.pos), lock)
211            }),
212        );
213        render_text_anchors(painter, text_anchors(&wire.labels, geometry));
214    }
215}