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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum LabelPass {
21 Draw,
22 Skip,
23 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 authoring: Authoring,
46 },
47 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
72const ROUTE_WIDTH: f32 = 1.7;
74const ROUTE_HOVER_WIDTH: f32 = 2.3;
76const ROUTE_SELECTION_HALO_WIDTH: f32 = 2.8;
79
80pub fn horizontal_label_anchor(pos: Pos2) -> Pos2 {
84 pos + vec2(0.0, -SHIM / 4.0)
85}
86
87pub 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
97pub 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
137pub 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
153pub 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#[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 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 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 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}