Skip to main content

blockworx_tools/
selection_bounds.rs

1//! World-space bounding boxes for the current selection. Shared by the selection
2//! frames (drawn on the canvas) and the selection overlay (placed in screen
3//! space), so both agree on what region a selection occupies.
4
5use blockworx_doc::id::{PinId, RouteId};
6use blockworx_geom::Rect;
7use blockworx_paint::Renderer;
8
9use crate::{
10    grid::GRID_SIZE, shape::ShapeId, theme::Style, tool::Deletable, widget::drawing::Drawing,
11};
12
13/// The union of the given shapes' `gui_rect`s, or `None` if none resolve.
14pub(crate) fn shapes_bounds(data: &Drawing, ids: &[ShapeId]) -> Option<Rect> {
15    ids.iter()
16        .filter_map(|&id| Some(data.shape(id)?.gui_rect()))
17        .reduce(Rect::union)
18}
19
20/// The union of the drawn extent (stub + name/type/tag labels) of each pin.
21/// Needs the painter to measure label text.
22pub(crate) fn pins_bounds(
23    data: &Drawing,
24    pins: &[PinId],
25    painter: &Style<'_, impl Renderer>,
26) -> Option<Rect> {
27    pins.iter()
28        .filter_map(|&anchor| {
29            let (shape, pin) = data.pin_on_shape(anchor)?;
30            let slot = crate::shape::pin::slot(pin);
31            Some(
32                crate::render::PinExtent {
33                    bbox: shape.gui_rect(),
34                    side: slot.side,
35                    offset: slot.offset,
36                    name: &pin.name,
37                    type_label: &pin.type_name,
38                    tag: &pin.tag,
39                }
40                .bbox(painter),
41            )
42        })
43        .reduce(Rect::union)
44}
45
46/// The bounding box of a route's polyline, expanded slightly so an axis-aligned
47/// route (zero-width or zero-height bbox) still yields a usable region.
48fn route_bounds(data: &Drawing, id: RouteId) -> Option<Rect> {
49    let pts = data.route_geometry(id)?.points();
50    (!pts.is_empty()).then(|| Rect::from_points(&pts).expand(GRID_SIZE / 2.0))
51}
52
53/// The world-space bounding box of whatever `sel` covers, or `None` if it can't
54/// be resolved. Used to place the selection overlay relative to the selection.
55pub fn selection_world_bounds(
56    sel: &Deletable,
57    data: &Drawing,
58    painter: &Style<'_, impl Renderer>,
59) -> Option<Rect> {
60    match sel {
61        Deletable::Shape(id) => shapes_bounds(data, std::slice::from_ref(id)),
62        Deletable::Shapes(ids) => shapes_bounds(data, ids),
63        Deletable::Pins(pins) => pins_bounds(data, pins, painter),
64        Deletable::Route(id) => route_bounds(data, *id),
65    }
66}