Skip to main content

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