1use crate::theme::Style;
2use blockworx_geom::{Rect, WorldPx, vec2};
3use blockworx_paint::{Interaction, Renderer};
4
5use crate::{
6 grid::{HIT_RADIUS, SHIM, px_point},
7 presentation::RouteDirection,
8 shape::ShapeId,
9 state::RenderMode,
10 theme::{Role, RoleStroke},
11 widget::drawing::Drawing,
12};
13
14#[tracing::instrument(level = "info", skip_all)]
19pub fn render(data: &Drawing, painter: &mut Style<'_, impl Renderer>) {
20 crate::widget::DrawingPasses::new(data).draw(painter);
21}
22
23pub struct RenderedLevel {
29 pub svg: String,
30 pub frame: Rect,
31 pub blocks: Vec<(blockworx_doc::id::BlockId, Rect)>,
32}
33
34pub fn render_svg(
38 theme: &crate::theme::Theme,
39 font: blockworx_paint::FontChoice,
40 indexed: blockworx_doc::document::IndexedDocument<'_>,
41 path: &crate::path::BlockPath,
42 presentation: &mut crate::presentation::Presentation,
43) -> String {
44 render_level(theme, font, indexed, path, presentation).svg
45}
46
47pub fn render_level(
50 theme: &crate::theme::Theme,
51 font: blockworx_paint::FontChoice,
52 indexed: blockworx_doc::document::IndexedDocument<'_>,
53 path: &crate::path::BlockPath,
54 presentation: &mut crate::presentation::Presentation,
55) -> RenderedLevel {
56 let mut gesture = crate::gesture::Gesture::idle();
58 let mut drawing = Drawing::new(indexed, path, presentation, &mut gesture);
59 let mut svg = crate::export::svg::SvgRenderer::new(theme.palette().clone(), font);
60 {
61 let mut style = Style::new(theme, &mut svg);
62 drawing.refresh_text_extents(&style);
65 render(&drawing, &mut style);
66 }
67 let blocks = drawing
68 .blocks_layer()
69 .filter_map(|(id, shape)| Some((id.block()?, shape.gui_rect())))
70 .collect();
71 let (svg, frame) = svg.finish();
72 RenderedLevel { svg, frame, blocks }
73}
74
75pub fn render_selected(data: &Drawing, selected: ShapeId, painter: &mut Style<'_, impl Renderer>) {
80 let authoring = data.authoring_of(selected);
81 crate::widget::DrawingPasses::new(data)
82 .shape_mode(move |id| {
83 if id == selected {
84 RenderMode::Selected { authoring }
85 } else {
86 RenderMode::Normal
87 }
88 })
89 .draw(painter);
90}
91
92pub fn widget(data: &Drawing, _interaction: &Interaction, painter: &mut Style<'_, impl Renderer>) {
93 render(data, painter);
94}
95
96pub fn render_hidden(data: &Drawing, hidden: ShapeId, painter: &mut Style<'_, impl Renderer>) {
100 crate::widget::DrawingPasses::new(data)
101 .shape_mode(move |id| {
102 if id == hidden {
103 RenderMode::Hidden
104 } else {
105 RenderMode::Normal
106 }
107 })
108 .draw(painter);
109}
110
111pub fn draw_text_bboxes(data: &Drawing, painter: &mut Style<'_, impl Renderer>) {
112 let stroke = RoleStroke::from((0.5, Role::DebugTextBbox));
113
114 for (_id, shape) in data.shapes() {
115 shape.with_pins(|pid, _pin| {
116 if let Some(r) = shape.pin_text_rect(pid, painter) {
117 painter.rect(r, WorldPx::ZERO, Role::Transparent, stroke);
118 }
119 });
120 if let Some(title) = shape.title() {
121 let title_size = painter.text_size(title.name, &painter.theme().title_font.clone());
122 let (title_pos, title_align) =
123 crate::render::clamped_block_title_position(shape.gui_rect(), &title, title_size.x);
124 let r = title_align.anchor_size(
125 title_pos,
126 vec2(
127 (title_size.x + 10.0).max(20.0),
128 crate::grid::TITLE_TEXT_SIZE,
129 ),
130 );
131 painter.rect(r, WorldPx::ZERO, Role::Transparent, stroke);
132 }
133 }
134
135 for (rid, wire) in data.auto_routes() {
136 let Some(geometry) = data.route_geometry(rid) else {
137 continue;
138 };
139 let measured =
140 painter.text_size(wire.route.name.clone(), &painter.theme().route_font.clone());
141 for &(_lid, label) in &wire.labels {
142 let loc = geometry.map_linear_distance_to_position(label);
143 let center = loc.location
144 + match loc.direction {
145 RouteDirection::Horizontal => vec2(0.0, -measured.y / 2.0 - SHIM / 4.0),
146 RouteDirection::Vertical => vec2(measured.y / 2.0 + SHIM / 4.0, 0.0),
147 };
148 let label_size = match loc.direction {
149 RouteDirection::Horizontal => measured,
150 RouteDirection::Vertical => vec2(measured.y, measured.x),
151 };
152 painter.rect(
153 Rect::from_center_size(center, label_size),
154 WorldPx::ZERO,
155 Role::Transparent,
156 stroke,
157 );
158 }
159 }
160}
161
162pub fn draw_resize_hit_targets(
166 data: &Drawing,
167 selected: &[ShapeId],
168 painter: &mut Style<'_, impl Renderer>,
169) {
170 let stroke = RoleStroke::from((0.5, Role::DebugMark));
171 for &id in selected {
172 let Some(shape) = data.shape(id) else {
173 continue;
174 };
175 if !shape.resizable() {
176 continue;
177 }
178 let r = shape.gui_rect();
179 for corner in [
180 r.left_top(),
181 r.right_top(),
182 r.left_bottom(),
183 r.right_bottom(),
184 ] {
185 painter.circle(corner, HIT_RADIUS, Role::Transparent, stroke);
186 }
187 }
188}
189
190pub fn draw_hit_targets(data: &Drawing, painter: &mut Style<'_, impl Renderer>) {
194 let stroke = RoleStroke::from((0.5, Role::DebugMark));
195 for (_id, shape) in data.shapes() {
196 let rect = shape.gui_rect();
197 shape.with_pins(|pid, _pin| {
198 if let Some(pos) = shape.anchor_point_with_rect(rect, pid) {
199 painter.circle(pos, HIT_RADIUS, Role::Transparent, stroke);
200 }
201 });
202 }
203 for (_rid, wire) in data.auto_routes() {
204 for wp in wire.route.waypoints.clone() {
205 painter.circle(px_point(wp.pos), HIT_RADIUS, Role::Transparent, stroke);
206 }
207 }
208}