Skip to main content

blockworx/shape/
mod.rs

1//! The geometry layer: where a shape's parts land on the canvas, how big
2//! they are, and how they paint.
3//!
4//! Deliberately document-*agnostic*. Every type here is handed the entities
5//! it draws — a block arrives with its pins, an image with its bytes, a text
6//! box with its measured extent — so nothing in this module looks anything
7//! up. The caller that knows the scope resolves those (see
8//! [`Drawing`](crate::widget::drawing::Drawing)), which is also what keeps a
9//! pin drawn as a boundary port and the same pin drawn as a stub on its
10//! block from needing two data shapes.
11
12use crate::theme::Style;
13use blockworx_geom::{Pos2, Rect, Vec2};
14use blockworx_paint::Renderer;
15
16use blockworx_doc::{
17    block_model::{Area, Asset, Pin, Text},
18    geometry::GridSize,
19    id::{AreaId, BlockId, ImageId, PinId, TextId},
20    values::LabelSide,
21};
22
23use crate::{shape::pin::PinSide, state::RenderMode};
24
25pub mod area;
26pub mod block;
27pub mod image;
28pub mod pin;
29pub mod port;
30pub mod text_box;
31
32pub use block::BlockShape;
33pub use image::Artwork;
34pub use port::PortShape;
35pub use text_box::TextShape;
36
37/// A unified ID that can refer to a child block (`BlockId`), a port-pin on the
38/// current block (`PinId`), a free-floating text annotation (`TextId`), a
39/// boundary area (`AreaId`), a free-floating background image
40/// (`ImageId`), or a block's foreground icon (keyed by the owning `BlockId`).
41#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
42pub enum ShapeId {
43    Rect(BlockId),
44    Port(PinId),
45    Text(TextId),
46    Area(AreaId),
47    Image(ImageId),
48    /// The icon owned by the block with this `BlockId` (at most one per block).
49    Icon(BlockId),
50}
51
52impl ShapeId {
53    /// The child block this id names, or `None` for every other kind —
54    /// [`Self::Icon`] included, which names the block that *owns* the icon
55    /// rather than the block itself.
56    pub fn block(self) -> Option<BlockId> {
57        match self {
58            Self::Rect(id) => Some(id),
59            Self::Port(_) | Self::Text(_) | Self::Area(_) | Self::Image(_) | Self::Icon(_) => None,
60        }
61    }
62
63    pub fn is_block(self) -> bool {
64        self.block().is_some()
65    }
66
67    /// Whether moving or resizing this shape can change routing. Only blocks
68    /// (obstacles) and ports (route endpoints) take part in the routing graph;
69    /// areas, text, and images are annotations, so manipulating them never
70    /// needs a reroute — and they never block another shape's move either.
71    pub fn affects_routing(self) -> bool {
72        matches!(self, Self::Rect(_) | Self::Port(_))
73    }
74
75    /// Whether this shape is artwork: a free-floating image or a block icon.
76    /// Artwork moves and resizes in free pixels — magnetically snapped to
77    /// alignment guides and to its intrinsic aspect ratio — instead of on the
78    /// grid pitch every other shape follows.
79    pub fn is_artwork(self) -> bool {
80        matches!(self, Self::Image(_) | Self::Icon(_))
81    }
82}
83
84#[derive(Copy, Clone, Debug)]
85pub struct PinLocation {
86    pub side: PinSide,
87    pub offset: f32,
88}
89
90impl From<(PinSide, f32)> for PinLocation {
91    fn from(value: (PinSide, f32)) -> Self {
92        PinLocation {
93            side: value.0,
94            offset: value.1,
95        }
96    }
97}
98
99/// A [`blockworx_doc::block_model::Label`] namespace resolved for drawing:
100/// its four registers read out,
101/// with the offset converted from the document's fixed-point to the world
102/// pixels every anchor formula works in
103/// ([`shape_label`](crate::edit::lower::shape_label) is the one conversion).
104/// Borrowed and `Copy`, so a drag preview builds its shifted twin with a
105/// struct update instead of cloning the name.
106#[derive(Clone, Copy, Debug, Default, PartialEq)]
107pub struct ShapeLabel<'a> {
108    pub name: &'a str,
109    pub hidden: bool,
110    pub side: LabelSide,
111    pub offset: f32,
112}
113
114/// `BaseShape` is what every shape can answer without knowing the document:
115/// where it sits, what its labels are, where its pins hang, and how it
116/// paints. Writes are not here — they are commits, emitted by
117/// [`crate::edit`] against the document the gesture read.
118pub trait BaseShape {
119    fn title(&self) -> Option<ShapeLabel<'_>> {
120        None
121    }
122    fn type_label(&self) -> Option<ShapeLabel<'_>> {
123        None
124    }
125    fn gui_rect(&self) -> Rect;
126    fn constrain_resize_delta(&self, delta: Vec2) -> Vec2 {
127        delta
128    }
129    fn pin(&self, id: PinId) -> Option<&Pin> {
130        let _ = id;
131        None
132    }
133    fn anchor_point_with_rect(&self, rect: Rect, id: PinId) -> Option<Pos2> {
134        let _ = (rect, id);
135        None
136    }
137    fn pin_position(&self, location: PinLocation) -> Option<Pos2> {
138        let _ = location;
139        None
140    }
141    /// The slot a dragged pin would snap to (its grid offset), or `None` if no
142    /// free slot is available. Read-only: the drop's own write is
143    /// [`crate::edit::geometry::move_pin`], and both resolve the same slot so
144    /// the preview lands where the commit will.
145    fn pin_drop_candidate(&self, pin_id: PinId, side: PinSide, raw_offset_px: f32) -> Option<u32> {
146        let _ = (pin_id, side, raw_offset_px);
147        None
148    }
149    fn pin_text_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
150        let _ = (id, painter);
151        None
152    }
153    /// Bounding box of a pin's `type` label (the smaller second line below the
154    /// name), for hit-testing a double-click on the type. `None` for shapes
155    /// without such a pin.
156    fn pin_type_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
157        let _ = (id, painter);
158        None
159    }
160    /// Bounding box of a pin's `tag` label (drawn above the stub line), sized for
161    /// an explicit `text`. `None` if the shape has no such pin. Callers pass the
162    /// "+tag" placeholder for an empty tag so the prompt stays a hit/edit target.
163    fn tag_text_rect_for<R: Renderer>(
164        &self,
165        id: PinId,
166        text: &str,
167        painter: &Style<'_, R>,
168    ) -> Option<Rect> {
169        let _ = (id, text, painter);
170        None
171    }
172    /// Clickable hit region around a pin's stub (the red line).
173    fn pin_stub_rect(&self, id: PinId) -> Option<Rect> {
174        let _ = id;
175        None
176    }
177    /// Render this shape. A block draws its own `tag` label from its
178    /// decorations, so no external id is needed.
179    fn render_ng<R: Renderer>(&self, mode: RenderMode, painter: &mut Style<'_, R>) {
180        let _ = (mode, painter);
181    }
182    fn new_pin_locations(&self) -> Vec<PinLocation> {
183        Vec::new()
184    }
185    fn title_anchor(&self) -> Option<Pos2> {
186        None
187    }
188    fn type_anchor(&self) -> Option<Pos2> {
189        None
190    }
191    fn resizable(&self) -> bool {
192        false
193    }
194}
195
196/// A borrowed reference to any shape, bundled with whatever the geometry
197/// layer needs beside the entity itself: a block's pins, a port's own id, a
198/// text box's measured extent, artwork's bytes. Because this is a concrete
199/// enum (not a trait object), methods with generic parameters (e.g.
200/// `with_pins`) work naturally — dispatch is via `match`, not a vtable.
201pub enum ShapeRef<'a> {
202    Block(BlockShape<'a>),
203    Port(PortShape<'a>),
204    Text(TextShape<'a>),
205    Area(&'a Area),
206    Image(Artwork<'a>),
207}
208
209impl<'a> ShapeRef<'a> {
210    /// A free-floating text annotation with the extent it was last measured
211    /// at, or `None` to fall back on the character-count estimate.
212    pub fn text(text: &'a Text, extent: Option<GridSize>) -> Self {
213        Self::Text(TextShape { text, extent })
214    }
215
216    /// A placed image or a block icon: the box it fills and the bytes behind
217    /// its content hash, which the caller resolved from the document's asset
218    /// table.
219    pub fn artwork(rect: Rect, asset: Option<&'a Asset>) -> Self {
220        Self::Image(Artwork { rect, asset })
221    }
222
223    pub fn gui_rect(&self) -> Rect {
224        match self {
225            Self::Block(b) => b.gui_rect(),
226            Self::Port(p) => p.gui_rect(),
227            Self::Text(t) => t.gui_rect(),
228            Self::Area(c) => c.gui_rect(),
229            Self::Image(s) => s.gui_rect(),
230        }
231    }
232    /// `accents` is the derived pin-accent lookup, resolved by the caller
233    /// (who knows the shape's scope); only ports consume it here — a block's
234    /// pins draw in the deferred [`Self::render_pins_ng`] pass.
235    pub fn render_ng<R: Renderer>(
236        &self,
237        accents: crate::presentation::ShapeAccents<'_>,
238        mode: RenderMode,
239        painter: &mut Style<'_, R>,
240    ) {
241        match self {
242            Self::Block(b) => b.render_ng(mode, painter),
243            Self::Port(p) => p.render_ng(accents, mode, painter),
244            Self::Text(t) => t.render_ng(mode, painter),
245            Self::Area(c) => c.render_ng(mode, painter),
246            Self::Image(s) => s.render_ng(mode, painter),
247        }
248    }
249    /// Draw only a block's deferred pin layer (see [`crate::widget::DrawingPasses`]).
250    /// Only blocks have pins to layer over their icon; every other shape draws
251    /// nothing.
252    pub fn render_pins_ng<R: Renderer>(
253        &self,
254        accents: crate::presentation::ShapeAccents<'_>,
255        mode: RenderMode,
256        painter: &mut Style<'_, R>,
257    ) {
258        match self {
259            Self::Block(b) => b.render_pins_ng(accents, mode, painter),
260            Self::Port(_) | Self::Text(_) | Self::Area(_) | Self::Image(_) => {}
261        }
262    }
263    pub fn with_pins(&self, mut f: impl FnMut(PinId, &'a Pin)) {
264        match self {
265            Self::Block(b) => b.pins.iter().for_each(|&(id, pin)| f(id, pin)),
266            Self::Port(p) => f(p.id, p.pin),
267            // Text boxes, areas, and images have no pins, so `f` is never called.
268            Self::Text(_) | Self::Area(_) | Self::Image(_) => {}
269        }
270    }
271    pub fn pin(&self, id: PinId) -> Option<&Pin> {
272        match self {
273            Self::Block(b) => b.pin(id),
274            Self::Port(p) => p.pin(id),
275            Self::Text(t) => t.pin(id),
276            Self::Area(c) => c.pin(id),
277            Self::Image(s) => s.pin(id),
278        }
279    }
280    pub fn pin_text_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
281        match self {
282            Self::Block(b) => b.pin_text_rect(id, painter),
283            Self::Port(p) => p.pin_text_rect(id, painter),
284            Self::Text(t) => t.pin_text_rect(id, painter),
285            Self::Area(c) => c.pin_text_rect(id, painter),
286            Self::Image(s) => s.pin_text_rect(id, painter),
287        }
288    }
289    pub fn pin_type_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
290        match self {
291            Self::Block(b) => b.pin_type_rect(id, painter),
292            Self::Port(p) => p.pin_type_rect(id, painter),
293            Self::Text(t) => t.pin_type_rect(id, painter),
294            Self::Area(c) => c.pin_type_rect(id, painter),
295            Self::Image(s) => s.pin_type_rect(id, painter),
296        }
297    }
298    pub fn tag_text_rect_for<R: Renderer>(
299        &self,
300        id: PinId,
301        text: &str,
302        painter: &Style<'_, R>,
303    ) -> Option<Rect> {
304        match self {
305            Self::Block(b) => b.tag_text_rect_for(id, text, painter),
306            Self::Port(p) => p.tag_text_rect_for(id, text, painter),
307            Self::Text(t) => t.tag_text_rect_for(id, text, painter),
308            Self::Area(c) => c.tag_text_rect_for(id, text, painter),
309            Self::Image(s) => s.tag_text_rect_for(id, text, painter),
310        }
311    }
312    pub fn pin_stub_rect(&self, id: PinId) -> Option<Rect> {
313        match self {
314            Self::Block(b) => b.pin_stub_rect(id),
315            Self::Port(p) => p.pin_stub_rect(id),
316            Self::Text(t) => t.pin_stub_rect(id),
317            Self::Area(c) => c.pin_stub_rect(id),
318            Self::Image(s) => s.pin_stub_rect(id),
319        }
320    }
321    pub fn anchor_point_with_rect(&self, rect: Rect, id: PinId) -> Option<Pos2> {
322        match self {
323            Self::Block(b) => b.anchor_point_with_rect(rect, id),
324            Self::Port(p) => p.anchor_point_with_rect(rect, id),
325            Self::Text(t) => t.anchor_point_with_rect(rect, id),
326            Self::Area(c) => c.anchor_point_with_rect(rect, id),
327            Self::Image(s) => s.anchor_point_with_rect(rect, id),
328        }
329    }
330    pub fn pin_drop_candidate(
331        &self,
332        pin_id: PinId,
333        side: PinSide,
334        raw_offset_px: f32,
335    ) -> Option<u32> {
336        match self {
337            Self::Block(b) => b.pin_drop_candidate(pin_id, side, raw_offset_px),
338            Self::Port(p) => p.pin_drop_candidate(pin_id, side, raw_offset_px),
339            Self::Text(t) => t.pin_drop_candidate(pin_id, side, raw_offset_px),
340            Self::Area(c) => c.pin_drop_candidate(pin_id, side, raw_offset_px),
341            Self::Image(s) => s.pin_drop_candidate(pin_id, side, raw_offset_px),
342        }
343    }
344    pub fn title(&self) -> Option<ShapeLabel<'_>> {
345        match self {
346            Self::Block(b) => b.title(),
347            Self::Port(p) => p.title(),
348            Self::Text(t) => t.title(),
349            Self::Area(c) => c.title(),
350            Self::Image(s) => s.title(),
351        }
352    }
353    pub fn type_label(&self) -> Option<ShapeLabel<'_>> {
354        match self {
355            Self::Block(b) => b.type_label(),
356            Self::Port(p) => p.type_label(),
357            Self::Text(t) => t.type_label(),
358            Self::Area(c) => c.type_label(),
359            Self::Image(s) => s.type_label(),
360        }
361    }
362    pub fn title_anchor(&self) -> Option<Pos2> {
363        match self {
364            Self::Block(b) => b.title_anchor(),
365            Self::Port(p) => p.title_anchor(),
366            Self::Text(t) => t.title_anchor(),
367            Self::Area(c) => c.title_anchor(),
368            Self::Image(s) => s.title_anchor(),
369        }
370    }
371    pub fn resizable(&self) -> bool {
372        match self {
373            Self::Block(b) => b.resizable(),
374            Self::Port(p) => p.resizable(),
375            Self::Text(t) => t.resizable(),
376            Self::Area(c) => c.resizable(),
377            Self::Image(s) => s.resizable(),
378        }
379    }
380    pub fn constrain_resize_delta(&self, delta: Vec2) -> Vec2 {
381        match self {
382            Self::Block(b) => b.constrain_resize_delta(delta),
383            Self::Port(p) => p.constrain_resize_delta(delta),
384            Self::Text(t) => t.constrain_resize_delta(delta),
385            Self::Area(c) => c.constrain_resize_delta(delta),
386            Self::Image(s) => s.constrain_resize_delta(delta),
387        }
388    }
389}