Skip to main content

blockworx_editor/render/
route.rs

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