1fn shape_layer_rank(id: ShapeId) -> u8 {
18 match id {
19 ShapeId::Rect(_) => 0,
20 ShapeId::Port(_) => 1,
21 ShapeId::Text(_) => 2,
22 ShapeId::Image(_) => 3,
23 ShapeId::Area(_) => 4,
24 ShapeId::Icon(_) => 5,
27 }
28}
29
30fn title_target(id: ShapeId) -> Option<crate::edit::naming::TitleTarget> {
34 use crate::edit::naming::TitleTarget;
35 match id {
36 ShapeId::Rect(id) => Some(TitleTarget::Block(id)),
37 ShapeId::Area(id) => Some(TitleTarget::Area(id)),
38 ShapeId::Port(_) | ShapeId::Text(_) | ShapeId::Image(_) | ShapeId::Icon(_) => None,
39 }
40}
41
42fn delete_target(id: ShapeId) -> Option<crate::edit::delete::Target> {
43 use crate::edit::delete::Target;
44 Some(match id {
45 ShapeId::Rect(id) => Target::Block(id),
46 ShapeId::Port(id) => Target::Pin(id),
47 ShapeId::Text(id) => Target::Text(id),
48 ShapeId::Area(id) => Target::Area(id),
49 ShapeId::Image(id) => Target::Image(id),
50 ShapeId::Icon(_) => return None,
51 })
52}
53
54use blockworx_doc::{
55 block_model::{Area, Asset, Block, Icon, Image, Pin, Route, Text},
56 commit::CommitBuilder,
57 document::{BlockIndex, Document as DocDocument, IndexedDocument, chronological},
58 entity::Entity,
59 geometry::{PinSlot, Waypoint},
60 hash::AssetHash,
61 id::{Allocator, AreaId, BlockId, Id, IdKind, ImageId, PinId, RouteId, RouteLabelId, TextId},
62 values::PinDir,
63};
64use blockworx_geom::{Pos2, Rect};
65
66use crate::edit::naming::Authoring;
67use crate::theme::Style;
68use crate::{
69 edit::{
70 self,
71 geometry::PinMove,
72 naming::{InterfaceLock, LabelFitWidth, TagVisibility},
73 },
74 grid::{artwork_rect, pin_slot},
75 path::BlockPath,
76 shape::{BlockShape, PinLocation, PortShape, ShapeId, ShapeRef},
77 tools::tool::{Deletable, RoleTarget},
78 widget::{
79 auto_route::{RouteGeometry, Wire, route_labels},
80 spatial::{HitId, SpatialIndex},
81 },
82};
83use blockworx_paint::Renderer;
84
85fn slot_at(location: PinLocation) -> PinSlot {
87 PinSlot {
88 side: location.side,
89 offset: pin_slot(location.offset),
90 }
91}
92
93#[derive(Default)]
99pub(crate) struct Supposed {
100 shapes: Vec<(ShapeId, Rect)>,
101 routes: Vec<RouteId>,
102}
103
104pub struct Drawing<'a> {
108 base: IndexedDocument<'a>,
113 path: &'a BlockPath,
114 index: Option<&'a SpatialIndex>,
119 pub(super) presentation: &'a mut crate::presentation::Presentation,
124 gesture: &'a mut crate::gesture::Gesture,
128 supposed: Supposed,
129}
130
131#[derive(Clone, Copy, Debug)]
135pub struct LabelPlacement {
136 pub offset: f32,
137 pub side: Option<blockworx_doc::values::LabelSide>,
138}
139
140impl From<LabelPlacement> for edit::naming::LabelPlacement {
141 fn from(placement: LabelPlacement) -> Self {
142 edit::naming::LabelPlacement {
143 offset: placement.offset.into(),
144 side: placement.side,
145 }
146 }
147}
148
149pub(crate) fn materialize_document(
163 indexed: &IndexedDocument<'_>,
164 presentation: &mut crate::presentation::Presentation,
165) {
166 let scopes: Vec<crate::path::Scope> = indexed
167 .index
168 .blocks
169 .iter()
170 .filter(|(_, entry)| !entry.routes.is_empty())
171 .map(|(&id, _)| crate::path::Scope::from_wire(id))
172 .collect();
173 presentation
177 .routes
178 .retain(|id, _| indexed.doc.route(id).is_some());
179 let mut discarded = crate::gesture::Gesture::idle();
180 for scope in scopes {
181 let mut path = BlockPath::empty();
182 if let crate::path::Scope::Block(id) = scope {
183 path.push(id);
184 }
185 Drawing::new(*indexed, &path, presentation, &mut discarded).materialize_routes();
186 }
187 debug_assert!(
188 discarded.ops().is_empty(),
189 "materialization is a read; it must author nothing"
190 );
191}
192
193impl<'a> Drawing<'a> {
194 pub fn new(
195 base: IndexedDocument<'a>,
196 path: &'a BlockPath,
197 presentation: &'a mut crate::presentation::Presentation,
198 gesture: &'a mut crate::gesture::Gesture,
199 ) -> Self {
200 let view = gesture.view(base);
201 presentation.refresh_accents(&view);
202 presentation.refresh_routes(&view);
203 Self {
204 base,
205 path,
206 index: None,
207 presentation,
208 gesture,
209 supposed: Supposed::default(),
210 }
211 }
212
213 pub(super) fn indexed(&self) -> IndexedDocument<'_> {
217 self.gesture.view(self.base)
218 }
219
220 pub(super) fn split(
224 &mut self,
225 ) -> (IndexedDocument<'_>, &mut crate::presentation::Presentation) {
226 (self.gesture.view(self.base), self.presentation)
227 }
228
229 pub fn writability(&self) -> blockworx_store::doc::Writability {
233 self.gesture.writability()
234 }
235
236 pub fn authoring(&self) -> Authoring {
239 self.writability().into()
240 }
241
242 pub fn authoring_of(&self, shape: ShapeId) -> Authoring {
245 Authoring::of(self.writability(), self.shape_owner_locked(shape).into())
246 }
247
248 pub fn unlocked_scope(&self, block: BlockId) -> Option<edit::lock::UnlockedScope> {
252 edit::lock::UnlockedScope::of(&self.indexed(), crate::path::Scope::Block(block))
253 }
254
255 pub(crate) fn mint<K: IdKind>(&mut self) -> Id<K> {
259 self.gesture.mint(self.base.doc)
260 }
261
262 pub(super) fn ids(&self) -> Allocator {
265 self.gesture.ids(self.base.doc)
266 }
267
268 pub(super) fn author(
272 &mut self,
273 what: &'static str,
274 emit: impl FnOnce(&IndexedDocument<'_>, &mut CommitBuilder),
275 ) {
276 self.gesture.author(self.base, what, emit);
277 }
278
279 pub fn new_indexed(
282 base: IndexedDocument<'a>,
283 path: &'a BlockPath,
284 index: &'a SpatialIndex,
285 presentation: &'a mut crate::presentation::Presentation,
286 gesture: &'a mut crate::gesture::Gesture,
287 ) -> Self {
288 let view = gesture.view(base);
289 presentation.refresh_accents(&view);
290 presentation.refresh_routes(&view);
291 Self {
292 base,
293 path,
294 index: Some(index),
295 presentation,
296 gesture,
297 supposed: Supposed::default(),
298 }
299 }
300
301 pub fn current_scope(&self) -> crate::path::Scope {
304 self.path.scope()
305 }
306
307 pub(super) fn scope(&self) -> Option<&BlockIndex> {
310 self.indexed().index.scope(self.current_scope().wire_id())
311 }
312
313 pub(super) fn current(&self) -> Option<&Block> {
316 self.current_scope()
317 .block()
318 .and_then(|id| self.held_block(id))
319 }
320
321 pub(super) fn held_block(&self, id: BlockId) -> Option<&Block> {
322 self.indexed().doc.block(&id)
323 }
324
325 pub(crate) fn held_pin(&self, id: PinId) -> Option<&Pin> {
326 self.indexed().doc.pin(&id)
327 }
328
329 pub(crate) fn pin_shape(&self, pin: PinId) -> Option<ShapeId> {
334 let owner = self.held_pin(pin)?.owner;
335 Some(
336 if crate::path::Scope::from_wire(owner) == self.current_scope() {
337 ShapeId::Port(pin)
338 } else {
339 ShapeId::Rect(owner)
340 },
341 )
342 }
343
344 pub(crate) fn block_shape(&self, id: BlockId) -> Option<BlockShape<'_>> {
348 match self.shape(ShapeId::Rect(id))? {
349 ShapeRef::Block(block) => Some(block),
350 _ => None,
351 }
352 }
353
354 pub(crate) fn pin_on_shape(&self, pin: PinId) -> Option<(ShapeRef<'_>, &Pin)> {
358 Some((self.shape(self.pin_shape(pin)?)?, self.held_pin(pin)?))
359 }
360
361 pub(crate) fn pin_owner_locked(&self, pin: PinId) -> bool {
365 self.held_pin(pin)
366 .and_then(|pin| self.held_block(pin.owner))
367 .is_some_and(|owner| owner.locked)
368 }
369
370 pub(crate) fn current_locked(&self) -> bool {
373 self.current().is_some_and(|b| b.locked)
374 }
375
376 pub(crate) fn shape_owner_locked(&self, id: ShapeId) -> bool {
380 match id {
381 ShapeId::Rect(rid) => self.held_block(rid).is_some_and(|b| b.locked),
382 ShapeId::Port(_) => self.current_locked(),
383 ShapeId::Text(_) | ShapeId::Area(_) | ShapeId::Image(_) | ShapeId::Icon(_) => false,
384 }
385 }
386
387 pub fn shape_accents(&self) -> crate::presentation::ShapeAccents<'_> {
391 crate::presentation::ShapeAccents::new(self.current_scope(), &self.presentation.pin_accents)
392 }
393
394 fn ordered<'s, K, T>(
403 &'s self,
404 ids: impl IntoIterator<Item = Id<K>>,
405 lookup: impl Fn(&'s DocDocument, &Id<K>) -> Option<&'s T>,
406 ) -> Vec<(Id<K>, &'s T)>
407 where
408 K: IdKind + Ord + Copy,
409 T: Entity + 's,
410 {
411 let doc = self.indexed().doc;
412 let held = |id: &Id<K>| lookup(doc, id);
413 let entries: Vec<(Id<K>, &T)> = ids
414 .into_iter()
415 .filter_map(|id| Some((id, held(&id)?)))
416 .collect();
417 chronological(entries.iter().copied())
418 .into_iter()
419 .filter_map(|id| Some((id, held(&id)?)))
420 .collect()
421 }
422
423 pub(super) fn child_blocks(&self) -> Vec<(BlockId, &Block)> {
425 crate::path::child_blocks(&self.indexed(), self.current_scope())
426 .into_iter()
427 .filter_map(|id| Some((id, self.held_block(id)?)))
428 .collect()
429 }
430
431 pub fn block(&self, id: BlockId) -> Option<&Block> {
434 self.scope()?
435 .children
436 .contains(&id)
437 .then(|| self.held_block(id))
438 .flatten()
439 }
440
441 pub(super) fn block_pins(&self, scope: crate::path::Scope) -> Vec<(PinId, &Pin)> {
445 let Some(entry) = self.indexed().index.scope(scope.wire_id()) else {
446 return Vec::new();
447 };
448 self.ordered(entry.pins.iter().copied(), |doc, id| doc.pin(id))
449 }
450
451 fn scope_texts(&self) -> Vec<(TextId, &Text)> {
452 let Some(scope) = self.scope() else {
453 return Vec::new();
454 };
455 self.ordered(scope.texts.iter().copied(), |doc, id| doc.text(id))
456 }
457
458 fn scope_areas(&self) -> Vec<(AreaId, &Area)> {
459 let Some(scope) = self.scope() else {
460 return Vec::new();
461 };
462 self.ordered(scope.areas.iter().copied(), |doc, id| doc.area(id))
463 }
464
465 fn scope_images(&self) -> Vec<(ImageId, &Image)> {
466 let Some(scope) = self.scope() else {
467 return Vec::new();
468 };
469 self.ordered(scope.images.iter().copied(), |doc, id| doc.image(id))
470 }
471
472 fn scope_owns(&self, owner: BlockId) -> bool {
476 self.scope().is_some() && crate::path::Scope::from_wire(owner) == self.current_scope()
477 }
478
479 fn scope_owned<'s, T: Entity>(
483 &'s self,
484 entity: Option<&'s T>,
485 owner: impl Fn(&T) -> BlockId,
486 ) -> Option<&'s T> {
487 entity.filter(|entity| self.scope_owns(owner(entity)))
488 }
489
490 fn asset(&self, hash: &AssetHash) -> Option<&Asset> {
493 self.indexed().doc.asset(hash)
494 }
495
496 pub fn delete(&mut self, what: Deletable) {
501 let targets: Vec<edit::delete::Target> = match what {
502 Deletable::Shape(id) => self.shape_targets(&[id]),
503 Deletable::Shapes(ids) => self.shape_targets(&ids),
504 Deletable::Route(rid) => vec![edit::delete::Target::Route(rid)],
505 Deletable::Pins(pins) => pins.into_iter().map(edit::delete::Target::Pin).collect(),
506 };
507 self.author("delete", |indexed, sink| {
508 edit::delete::selection(indexed, &targets, sink);
509 });
510 }
511
512 fn shape_targets(&mut self, shapes: &[ShapeId]) -> Vec<edit::delete::Target> {
516 for &id in shapes {
517 if let ShapeId::Icon(block) = id {
518 self.author("shape_targets", |indexed, sink| {
519 edit::assets::delete_icon(indexed.doc, block, sink);
520 });
521 }
522 }
523 shapes.iter().filter_map(|&id| delete_target(id)).collect()
524 }
525
526 pub(super) fn scope_route_ids(&self) -> Vec<RouteId> {
532 let Some(scope) = self.scope() else {
533 return Vec::new();
534 };
535 let doc = self.indexed().doc;
536 let held: Vec<(RouteId, &Route)> = scope
537 .routes
538 .iter()
539 .filter_map(|&id| Some((id, doc.route(&id)?)))
540 .collect();
541 chronological(held.iter().copied())
542 }
543
544 pub(super) fn route(&self, id: RouteId) -> Option<&Route> {
545 self.indexed().doc.route(&id)
546 }
547
548 pub fn auto_routes(&self) -> impl Iterator<Item = (RouteId, Wire<'_>)> {
549 self.scope_route_ids()
550 .into_iter()
551 .filter_map(|id| Some((id, self.auto_route(id)?)))
552 .collect::<Vec<_>>()
553 .into_iter()
554 }
555
556 pub fn auto_route(&self, id: RouteId) -> Option<Wire<'_>> {
560 let route = self
561 .route(id)
562 .filter(|route| self.scope_owns(route.owner))?;
563 Some(Wire {
564 route,
565 labels: route_labels(&self.indexed(), id),
566 })
567 }
568 pub fn add_route(
574 &mut self,
575 from: PinId,
576 to: crate::edit::create::RouteEnd,
577 waypoints: Vec<Waypoint>,
578 ) -> RouteId {
579 let id = self.mint();
580 let owner = self.current_scope();
581 self.author("add_route", |indexed, sink| {
582 edit::create::route(
583 indexed,
584 edit::create::NewRoute {
585 id,
586 owner,
587 from,
588 to,
589 waypoints,
590 },
591 sink,
592 );
593 });
594 id
595 }
596 pub fn route_geometry(&self, id: RouteId) -> Option<&RouteGeometry> {
599 self.presentation.routes.get(&id)
600 }
601
602 pub fn add_image(&mut self, placement: edit::assets::Placement, asset: &Asset) -> ShapeId {
607 let id: ImageId = self.mint();
608 let owner = self.current_scope();
609 self.author("add_image", |indexed, sink| {
610 edit::assets::image(
611 indexed.doc,
612 edit::assets::NewImage {
613 id,
614 owner,
615 placement,
616 },
617 asset,
618 sink,
619 );
620 });
621 ShapeId::Image(id)
622 }
623 #[cfg_attr(not(test), allow(dead_code))]
624 pub fn image(&self, id: ImageId) -> Option<&Image> {
625 self.scope_owned(self.indexed().doc.image(&id), |i| i.owner)
626 }
627
628 pub fn icon(&self, id: BlockId) -> Option<&Icon> {
633 self.block(id)
634 .and_then(|b| crate::edit::geometry::artwork(&b.icon))
635 }
636 pub fn set_icon(&mut self, id: BlockId, asset: &Asset) {
639 self.author("set_icon", |indexed, sink| {
640 edit::assets::set_icon(indexed.doc, id, asset, sink);
641 });
642 }
643
644 pub fn add_block(&mut self, start: Pos2, end: Pos2) -> BlockId {
647 let id = self.mint();
648 let scope = self.current_scope();
649 self.author("add_block", |indexed, sink| {
650 edit::create::block(
651 indexed.doc,
652 edit::create::NewBlock {
653 id,
654 scope,
655 start,
656 end,
657 },
658 sink,
659 );
660 });
661 id
662 }
663 pub fn add_rect_box(&mut self, start: Pos2, end: Pos2) -> BlockId {
665 self.add_block(start, end)
666 }
667
668 pub fn add_text_box(&mut self, pos: Pos2) -> TextId {
674 let id = self.mint();
675 let scope = self.current_scope();
676 self.author("add_text_box", |_indexed, sink| {
677 edit::create::text_box(id, scope, pos, sink);
678 });
679 id
680 }
681 pub fn text_box(&self, id: TextId) -> Option<&Text> {
682 self.scope_owned(self.indexed().doc.text(&id), |t| t.owner)
683 }
684
685 pub fn add_area(&mut self, start: Pos2, end: Pos2) -> ShapeId {
691 let id: AreaId = self.mint();
692 let scope = self.current_scope();
693 self.author("add_area", |_indexed, sink| {
694 edit::create::area(id, scope, start, end, sink);
695 });
696 ShapeId::Area(id)
697 }
698 pub fn areas(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
702 self.scope_areas()
703 .into_iter()
704 .map(|(id, c)| (ShapeId::Area(id), ShapeRef::Area(c)))
705 }
706
707 pub fn add_port_auto_named(&mut self, inner: Rect) -> PinId {
716 let id = self.mint();
717 let scope = self.current_scope();
718 self.author("add_port_auto_named", |indexed, sink| {
719 let Some(owner) = edit::lock::UnlockedScope::of(indexed, scope) else {
721 return;
722 };
723 edit::create::port(
724 indexed,
725 edit::create::NewPort {
726 id,
727 owner,
728 start: inner.min,
729 end: inner.max,
730 },
731 sink,
732 );
733 });
734 id
735 }
736
737 pub fn shape(&self, id: ShapeId) -> Option<ShapeRef<'_>> {
741 match id {
742 ShapeId::Rect(rid) => Some(ShapeRef::Block(self.shape_of_block(rid, self.block(rid)?))),
743 ShapeId::Port(pid) => {
746 let pin = self
747 .held_pin(pid)
748 .filter(|pin| self.scope_owns(pin.owner))?;
749 Some(ShapeRef::Port(PortShape { id: pid, pin }))
750 }
751 ShapeId::Text(tid) => {
752 let text = self.scope_owned(self.indexed().doc.text(&tid), |t| t.owner)?;
753 let extent = self.presentation.text_extents.valid_for(tid, &text.text);
754 Some(ShapeRef::text(text, extent))
755 }
756 ShapeId::Area(cid) => self
757 .scope_owned(self.indexed().doc.area(&cid), |c| c.owner)
758 .map(ShapeRef::Area),
759 ShapeId::Image(sid) => {
760 let image = self.scope_owned(self.indexed().doc.image(&sid), |i| i.owner)?;
761 Some(ShapeRef::artwork(
762 artwork_rect(image.rect),
763 self.asset(&image.asset),
764 ))
765 }
766 ShapeId::Icon(rid) => {
767 let icon = crate::edit::geometry::artwork(&self.block(rid)?.icon)?;
768 Some(ShapeRef::artwork(
769 artwork_rect(icon.rect),
770 self.asset(&icon.asset),
771 ))
772 }
773 }
774 }
775 pub fn shape_tag_hidden(&self, id: ShapeId) -> Option<bool> {
779 match id {
780 ShapeId::Port(pid) => Some(self.shape(id)?.pin(pid)?.tag_hidden),
782 ShapeId::Rect(_)
783 | ShapeId::Text(_)
784 | ShapeId::Area(_)
785 | ShapeId::Image(_)
786 | ShapeId::Icon(_) => None,
787 }
788 }
789
790 pub fn set_shape_tag_hidden(&mut self, id: ShapeId, tag: TagVisibility) {
793 if let ShapeId::Port(pid) = id {
795 self.author("set_shape_tag_hidden", |indexed, sink| {
796 edit::naming::set_tag_visibility(indexed.doc, &[pid], tag, sink);
797 });
798 }
799 }
800
801 pub fn set_role(&mut self, target: RoleTarget, role: Option<u8>) {
804 let target = match target {
805 RoleTarget::Block(id) => edit::naming::AccentTarget::Block(id),
806 RoleTarget::Port(id) => edit::naming::AccentTarget::Port(id),
807 RoleTarget::Route(id) => edit::naming::AccentTarget::Route(id),
808 RoleTarget::Area(id) => edit::naming::AccentTarget::Area(id),
809 RoleTarget::Text(id) => edit::naming::AccentTarget::Text(id),
810 };
811 self.author("set_role", |indexed, sink| {
812 edit::naming::set_accent(indexed.doc, target, role, sink);
813 });
814 }
815
816 pub fn set_pins_kind(&mut self, pins: &[PinId], dir: PinDir) {
819 self.author("set_pins_kind", |indexed, sink| {
820 let targets = edit::lock::MaterialPin::all(indexed.doc, pins);
821 edit::naming::set_dirs(indexed.doc, &targets, dir, sink);
822 });
823 }
824
825 pub fn cycle_pin_kind(&mut self, pin: PinId) {
828 self.author("cycle_pin_kind", |indexed, sink| {
829 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
830 return;
831 };
832 edit::naming::cycle_dir(indexed.doc, target, sink);
833 });
834 }
835
836 pub fn set_pins_tag_hidden(&mut self, pins: &[PinId], tag: TagVisibility) {
838 self.author("set_pins_tag_hidden", |indexed, sink| {
839 edit::naming::set_tag_visibility(indexed.doc, pins, tag, sink);
840 });
841 }
842
843 pub fn set_block_locked(&mut self, block: BlockId, lock: InterfaceLock) {
845 self.author("set_block_locked", |indexed, sink| {
846 edit::naming::set_locked(indexed.doc, block, lock, sink);
847 });
848 }
849
850 pub fn set_title_text(&mut self, shape: ShapeId, text: &str) {
853 let Some(target) = title_target(shape) else {
854 return;
855 };
856 self.author("set_title_text", |indexed, sink| {
857 edit::naming::rename_title(indexed.doc, target, text, sink);
858 });
859 }
860
861 pub fn place_title(&mut self, shape: ShapeId, placement: LabelPlacement) {
863 let target = match shape {
864 ShapeId::Rect(id) => edit::naming::LabelTarget::BlockTitle(id),
865 ShapeId::Area(id) => edit::naming::LabelTarget::AreaTitle(id),
866 _ => return,
867 };
868 self.author("place_title", |indexed, sink| {
869 edit::naming::place_label(indexed.doc, target, placement.into(), sink);
870 });
871 }
872
873 pub fn set_type_label_text(&mut self, rect: BlockId, text: &str) {
875 self.author("set_type_label_text", |indexed, sink| {
876 edit::naming::rename_type(indexed.doc, rect, text, sink);
877 });
878 }
879
880 pub fn place_type_label(&mut self, rect: BlockId, placement: LabelPlacement) {
882 self.author("place_type_label", |indexed, sink| {
883 edit::naming::place_label(
884 indexed.doc,
885 edit::naming::LabelTarget::BlockType(rect),
886 placement.into(),
887 sink,
888 );
889 });
890 }
891
892 pub fn label_fit(
896 &self,
897 pin: PinId,
898 name: Option<&str>,
899 type_name: Option<&str>,
900 ) -> LabelFitWidth {
901 let held = self.held_pin(pin);
902 let name = name.unwrap_or_else(|| held.map_or("", |pin| &pin.name));
903 let type_name = type_name.unwrap_or_else(|| held.map_or("", |pin| &pin.type_name));
904 LabelFitWidth::new(crate::shape::port::width_for_labels(name, type_name))
905 }
906
907 pub fn rename_pin(&mut self, pin: PinId, text: &str, fit: LabelFitWidth) {
910 self.author("rename_pin", |indexed, sink| {
911 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
912 return;
913 };
914 edit::naming::rename_pin(indexed.doc, target, text, fit, sink);
915 });
916 }
917
918 pub fn set_pin_tag(&mut self, pin: PinId, text: &str) {
921 self.author("set_pin_tag", |indexed, sink| {
922 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
923 return;
924 };
925 edit::naming::set_tag(indexed.doc, target, text, sink);
926 });
927 }
928
929 pub fn retype_pin(&mut self, pin: PinId, text: &str, fit: LabelFitWidth) {
932 self.author("retype_pin", |indexed, sink| {
933 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
934 return;
935 };
936 edit::naming::retype_pin(indexed.doc, target, text, fit, sink);
937 });
938 }
939
940 pub fn add_named_pin(&mut self, block: BlockId, loc: PinLocation) -> Option<PinId> {
944 if edit::lock::UnlockedScope::of(&self.indexed(), crate::path::Scope::Block(block))
947 .is_none()
948 {
949 tracing::debug!(target: "edit", edit = "add_named_pin", "declined");
950 return None;
951 }
952 let id = self.mint();
953 self.author("add_named_pin", |indexed, sink| {
954 let Some(owner) =
955 edit::lock::UnlockedScope::of(indexed, crate::path::Scope::Block(block))
956 else {
957 return;
958 };
959 edit::create::pin(
960 indexed,
961 edit::create::NewPin {
962 id,
963 owner,
964 slot: slot_at(loc),
965 },
966 sink,
967 );
968 });
969 Some(id)
970 }
971
972 pub fn apply_resize(&mut self, shape: ShapeId, new_rect: Rect) {
976 let target = match shape {
977 ShapeId::Rect(id) => edit::geometry::ResizeTarget::Block(id),
978 ShapeId::Port(id) => edit::geometry::ResizeTarget::Port(id),
979 ShapeId::Area(id) => edit::geometry::ResizeTarget::Area(id),
980 ShapeId::Image(id) => edit::geometry::ResizeTarget::Image(id),
981 ShapeId::Icon(id) => edit::geometry::ResizeTarget::Icon(id),
982 ShapeId::Text(_) => return,
985 };
986 self.author("apply_resize", |indexed, sink| {
987 edit::geometry::resize(indexed, target, new_rect, sink);
988 });
989 }
990
991 pub fn move_pin_snapped(&mut self, pin: PinId, to: PinSlot) {
996 self.author("move_pin_snapped", |indexed, sink| {
997 edit::geometry::move_pin(indexed, pin, to, sink);
998 });
999 }
1000
1001 pub fn set_route_name(&mut self, route: RouteId, label: RouteLabelId, text: &str) {
1005 if text.trim().is_empty() {
1006 self.author("set_route_name", |indexed, sink| {
1007 edit::naming::clear_wire_label(indexed.doc, label, sink);
1008 });
1009 } else {
1010 self.author("set_route_name", |indexed, sink| {
1011 edit::naming::rename_route(indexed.doc, route, text, sink);
1012 });
1013 }
1014 }
1015
1016 pub fn set_text_box_content(&mut self, id: TextId, text: &str) {
1024 self.author("set_text_box_content", |indexed, sink| {
1025 edit::naming::edit_text(indexed.doc, id, text, sink);
1026 });
1027 }
1028
1029 pub fn refresh_text_extents(&mut self, painter: &Style<'_, impl Renderer>) {
1040 let stale: Vec<(TextId, String)> = self
1041 .scope_texts()
1042 .into_iter()
1043 .filter(|(id, text)| {
1044 self.presentation
1045 .text_extents
1046 .valid_for(*id, &text.text)
1047 .is_none()
1048 })
1049 .map(|(id, text)| (id, text.text.clone()))
1050 .collect();
1051 for (id, text) in stale {
1052 let size = crate::render::text_box::measure_box_size(painter, &text);
1055 self.presentation.text_extents.set(id, text, size);
1056 }
1057 }
1058
1059 pub fn add_route_label(&mut self, route: RouteId, pos: Pos2) -> Option<RouteLabelId> {
1062 let distance = self.route_geometry(route)?.distance_along(pos);
1063 let id = self.mint();
1064 self.author("add_route_label", |indexed, sink| {
1065 edit::create::wire_label(indexed.doc, id, route, distance, sink);
1066 });
1067 Some(id)
1068 }
1069
1070 pub fn place_route_label(
1073 &mut self,
1074 label: RouteLabelId,
1075 dist: blockworx_doc::geometry::FracVal,
1076 ) {
1077 self.author("place_route_label", |indexed, sink| {
1078 edit::geometry::place_wire_label(indexed.doc, label, dist, sink);
1079 });
1080 }
1081
1082 pub fn flip_shape_pins(&mut self, id: ShapeId) {
1092 let target = match id {
1093 ShapeId::Rect(rid) => edit::geometry::FlipTarget::Block(rid),
1094 ShapeId::Port(pid) => edit::geometry::FlipTarget::Port(pid),
1095 ShapeId::Text(_) | ShapeId::Area(_) | ShapeId::Image(_) | ShapeId::Icon(_) => return,
1096 };
1097 self.author("flip_shape_pins", |indexed, sink| {
1098 edit::geometry::flip_pins(indexed, target, sink);
1099 });
1100 }
1101
1102 #[expect(
1110 dead_code,
1111 reason = "unhooked from Go Up until it is given a home of its own"
1112 )]
1113 pub fn wrap_top(&mut self) {
1114 let id = self.mint();
1115 self.author("wrap_top", |indexed, sink| {
1116 edit::create::wrap_top(indexed, id, sink);
1117 });
1118 }
1119
1120 pub fn flip_block_vertical(&mut self, block: BlockId) {
1126 self.author("flip_block_vertical", |indexed, sink| {
1127 edit::geometry::flip_vertical(indexed, block, sink);
1128 });
1129 }
1130
1131 pub fn current_blocks(&self) -> impl Iterator<Item = (BlockId, &Block)> {
1134 self.child_blocks().into_iter()
1135 }
1136
1137 fn shape_of_block<'s>(&'s self, id: BlockId, block: &'s Block) -> BlockShape<'s> {
1141 BlockShape::new(
1142 block,
1143 self.block_pins(crate::path::Scope::Block(id)),
1144 crate::path::structure(&self.indexed(), id),
1145 )
1146 }
1147
1148 pub fn blocks_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1151 self.child_blocks().into_iter().map(|(id, block)| {
1152 (
1153 ShapeId::Rect(id),
1154 ShapeRef::Block(self.shape_of_block(id, block)),
1155 )
1156 })
1157 }
1158 pub fn ports_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1161 self.block_pins(self.current_scope())
1162 .into_iter()
1163 .map(|(id, pin)| (ShapeId::Port(id), ShapeRef::Port(PortShape { id, pin })))
1164 }
1165 pub fn texts_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1167 self.scope_texts().into_iter().map(|(id, text)| {
1168 let extent = self.presentation.text_extents.valid_for(id, &text.text);
1169 (ShapeId::Text(id), ShapeRef::text(text, extent))
1170 })
1171 }
1172 pub fn images_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1174 self.scope_images().into_iter().map(|(id, image)| {
1175 (
1176 ShapeId::Image(id),
1177 ShapeRef::artwork(artwork_rect(image.rect), self.asset(&image.asset)),
1178 )
1179 })
1180 }
1181 pub fn icons(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1185 self.child_blocks().into_iter().filter_map(|(id, block)| {
1186 let icon = crate::edit::geometry::artwork(&block.icon)?;
1187 Some((
1188 ShapeId::Icon(id),
1189 ShapeRef::artwork(artwork_rect(icon.rect), self.asset(&icon.asset)),
1190 ))
1191 })
1192 }
1193 pub fn shapes(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1197 self.blocks_layer()
1198 .chain(self.ports_layer())
1199 .chain(self.texts_layer())
1200 .chain(self.images_layer())
1201 }
1202
1203 pub fn can_relocate_pins(&self, moves: &[PinMove]) -> bool {
1208 crate::edit::geometry::relocation_fits(&self.indexed(), moves)
1209 }
1210
1211 pub fn relocate_pins(&mut self, moves: &[PinMove]) -> bool {
1214 let applied = self.can_relocate_pins(moves);
1215 self.author("relocate_pins", |indexed, sink| {
1216 edit::geometry::relocate_pins(indexed, moves, sink);
1217 });
1218 applied
1219 }
1220
1221 pub fn nudge_pins(&mut self, pins: &[PinId], slot_delta: i32) {
1226 self.author("nudge_pins", |indexed, sink| {
1227 edit::geometry::nudge_pins(
1228 indexed,
1229 pins,
1230 edit::geometry::SlotDelta::new(slot_delta),
1231 sink,
1232 );
1233 });
1234 }
1235
1236 pub fn content_bounds(&self, painter: &Style<'_, impl Renderer>) -> Option<Rect> {
1242 let mut extent = blockworx_paint::Extent::measuring_through(painter.renderer());
1243 crate::widget::DrawingPasses::new(self).draw(&mut Style::new(painter.theme(), &mut extent));
1244 extent.finish()
1245 }
1246
1247 fn draw_key(id: ShapeId) -> (u8, ShapeId) {
1260 (shape_layer_rank(id), id)
1261 }
1262
1263 pub(crate) fn shape_candidates(&self, query: Rect) -> Vec<(ShapeId, ShapeRef<'_>)> {
1269 match self.index {
1270 Some(idx) => {
1271 let mut v: Vec<(ShapeId, ShapeRef<'_>)> = idx
1272 .in_rect(query)
1273 .filter_map(|hit| match hit {
1274 HitId::Shape(id) if !matches!(id, ShapeId::Area(_) | ShapeId::Icon(_)) => {
1280 self.shape(id).map(|s| (id, s))
1281 }
1282 _ => None,
1283 })
1284 .collect();
1285 v.sort_by_key(|(id, _)| Self::draw_key(*id));
1286 v
1287 }
1288 None => self.shapes().collect(),
1289 }
1290 }
1291
1292 pub(crate) fn hit_candidates(&self, query: Rect) -> Vec<(ShapeId, ShapeRef<'_>)> {
1297 let mut v = self.shape_candidates(query);
1298 v.reverse();
1299 v
1300 }
1301
1302 pub(super) fn suppose_shapes(&mut self, previews: &[(ShapeId, Rect)]) {
1306 self.supposed.shapes.clear();
1307 for &(id, rect) in previews {
1308 self.supposed.shapes.push((id, rect));
1309 if let Some(icon) = id.block().map(ShapeId::Icon)
1310 && self.shape(icon).is_some()
1311 {
1312 self.supposed.shapes.push((icon, rect));
1313 }
1314 }
1315 }
1316
1317 pub(super) fn suppose_routes(&mut self, routes: Vec<RouteId>) {
1320 self.supposed.routes = routes;
1321 }
1322
1323 pub(crate) fn visible_ids(&self, viewport: Rect) -> Option<std::collections::HashSet<HitId>> {
1334 let idx = self.index?;
1335 let mut ids: std::collections::HashSet<HitId> = idx.in_rect(viewport).collect();
1336 for &(id, rect) in &self.supposed.shapes {
1337 if self.shape(id).is_some_and(|shape| {
1338 crate::render::bounds::shape_bounds_at(&shape, rect).intersects(viewport)
1339 }) {
1340 ids.insert(HitId::Shape(id));
1341 }
1342 }
1343 for &id in &self.supposed.routes {
1344 if self
1345 .auto_route(id)
1346 .zip(self.route_geometry(id))
1347 .is_some_and(|(wire, geometry)| {
1348 crate::render::bounds::route_bounds(&wire, geometry).intersects(viewport)
1349 })
1350 {
1351 ids.insert(HitId::Route(id));
1352 }
1353 }
1354 Some(ids)
1355 }
1356
1357 pub(crate) fn route_candidates(&self, query: Rect) -> Vec<(RouteId, Wire<'_>)> {
1362 match self.index {
1363 Some(idx) => {
1364 let order = self.scope_route_ids();
1365 let mut v: Vec<(RouteId, Wire<'_>)> = idx
1366 .in_rect(query)
1367 .filter_map(|hit| match hit {
1368 HitId::Route(id) => self.auto_route(id).map(|r| (id, r)),
1369 HitId::Shape(_) => None,
1370 })
1371 .collect();
1372 v.sort_by_key(|(id, _)| {
1373 order
1374 .iter()
1375 .position(|other| other == id)
1376 .unwrap_or(usize::MAX)
1377 });
1378 v
1379 }
1380 None => self.auto_routes().collect(),
1381 }
1382 }
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387 use super::*;
1388 use crate::path::Scope;
1389 use crate::{
1390 edit::lower::slot_capacity,
1391 grid::GRID_SIZE,
1392 tools::tool::Deletable,
1393 widget::test_fixtures::{self as fx, Scene},
1394 };
1395 use blockworx_doc::{
1396 fixtures::{block_id, pin_id, route_id},
1397 values::PinSide as DocPinSide,
1398 };
1399 use blockworx_geom::{pos2, vec2};
1400
1401 #[test]
1405 fn a_tool_mints_one_past_the_highest_id_the_document_holds() {
1406 let mut scene = Scene::new(vec![fx::block(7, 0.0), fx::pin(2, 7, DocPinSide::East, 0)]);
1407 assert!(
1408 scene.drawing().held_block(block_id(8)).is_none(),
1409 "precondition: b8 is free before the gesture",
1410 );
1411
1412 let (made, _) = scene.authored(|drawing| {
1413 (
1414 drawing.add_block(pos2(200.0, 0.0), pos2(240.0, 40.0)),
1415 drawing.add_text_box(pos2(300.0, 0.0)),
1416 )
1417 });
1418 assert_eq!(
1419 made,
1420 (block_id(8), blockworx_doc::fixtures::text_id(1)),
1421 "the block counts on from b7 while the texts start their own space at 1",
1422 );
1423
1424 scene.authored(|drawing| drawing.delete(Deletable::Shape(ShapeId::Rect(block_id(8)))));
1425 assert!(
1426 scene.drawing().held_block(block_id(8)).is_none(),
1427 "precondition: b8 is deleted",
1428 );
1429 let (again, _) =
1430 scene.authored(|drawing| drawing.add_block(pos2(0.0, 200.0), pos2(40.0, 240.0)));
1431 assert_eq!(
1432 again,
1433 block_id(9),
1434 "the marks never fall, so the next block cannot land on a departed id",
1435 );
1436 }
1437
1438 fn wire_outside_the_blocks() -> Scene {
1442 Scene::new(vec![
1443 fx::block(1, 0.0),
1444 fx::block(2, 120.0),
1445 fx::pin(3, 1, DocPinSide::East, 0),
1446 fx::pin(4, 2, DocPinSide::West, 0),
1447 fx::route(5, Scope::Root, 3, 4, &[(8, -30), (8, -30)]),
1448 ])
1449 }
1450
1451 #[test]
1455 fn the_content_bounds_hold_the_wires_too() {
1456 let mut scene = wire_outside_the_blocks();
1457 let theme = crate::theme::Theme::default();
1458 let mut backend = crate::export::svg::SvgRenderer::new(
1459 theme.palette().clone(),
1460 blockworx_paint::FontChoice::default(),
1461 );
1462 let style = Style::new(&theme, &mut backend);
1463 let drawing = scene.drawing();
1464
1465 let blocks = drawing
1466 .blocks_layer()
1467 .map(|(_, shape)| shape.gui_rect())
1468 .reduce(Rect::union)
1469 .expect("the scene holds blocks");
1470 let wire = drawing
1471 .auto_routes()
1472 .filter_map(|(id, _)| drawing.route_geometry(id))
1473 .flat_map(|geometry| geometry.points())
1474 .fold(Rect::NOTHING, |acc, p| acc.union(Rect::from_min_max(p, p)));
1475 assert!(
1476 wire.is_positive() && !blocks.contains_rect(wire),
1477 "precondition: the wire must escape the blocks it joins \
1478 (wire {wire:?}, blocks {blocks:?})",
1479 );
1480
1481 let bounds = drawing
1482 .content_bounds(&style)
1483 .expect("the scene draws something");
1484 assert!(
1485 bounds.contains_rect(wire),
1486 "the fit cropped the wire: {bounds:?} does not hold {wire:?}",
1487 );
1488 }
1489
1490 #[test]
1500 fn a_gesture_reads_the_block_it_just_created() {
1501 let mut scene = Scene::new(vec![]);
1502 let (created, staged) = scene.commit(|drawing| {
1503 let created = drawing.add_rect_box(pos2(0.0, 0.0), pos2(40.0, 40.0));
1504 let seen = drawing
1505 .shape(ShapeId::Rect(created))
1506 .expect("the gesture's own block is in the document it reads");
1507 assert!(
1508 seen.title().is_some(),
1509 "including the title an editor would be seeded from",
1510 );
1511 (created, seen.gui_rect())
1512 });
1513 let settled = scene
1514 .drawing()
1515 .shape(ShapeId::Rect(created))
1516 .expect("the sealed commit folded")
1517 .gui_rect();
1518 assert_eq!(
1519 staged, settled,
1520 "the geometry read mid-gesture is the geometry the commit lands",
1521 );
1522 assert!(
1523 staged.area() > 0.0,
1524 "and it is a real rect, so the comparison is not two empties",
1525 );
1526 }
1527
1528 #[test]
1532 fn a_gesture_reads_nothing_it_did_not_write() {
1533 let mut scene = Scene::new(vec![tall_block(1)]);
1534 scene.commit(|drawing| {
1535 assert!(
1536 drawing.shape(ShapeId::Rect(block_id(1))).is_some(),
1537 "the scene's own block is there",
1538 );
1539 assert!(
1540 drawing.shape(ShapeId::Rect(block_id(2))).is_none(),
1541 "a block no one authored is not",
1542 );
1543 });
1544 }
1545
1546 #[test]
1555 fn a_text_boxs_extent_is_measured_for_whatever_text_it_holds() {
1556 fn frame(
1559 ctx: &egui::Context,
1560 scene: &mut Scene,
1561 id: TextId,
1562 ) -> Option<blockworx_doc::geometry::GridSize> {
1563 let mut out = None;
1564 ctx.run_ui(egui::RawInput::default(), |ui| {
1565 let theme = crate::theme::Theme::default();
1566 let mut painter =
1567 crate::canvas::Painter::headless(ui.painter().clone(), theme.palette().clone());
1568 let style = crate::theme::Style::new(&theme, &mut painter);
1569 let mut drawing = scene.drawing();
1570 drawing.refresh_text_extents(&style);
1571 let Some(text) = drawing.text_box(id).map(|t| t.text.clone()) else {
1572 return;
1573 };
1574 out = drawing.presentation.text_extents.valid_for(id, &text);
1575 })
1576 .drop_without_applying_deltas();
1577 out
1578 }
1579
1580 let id = blockworx_doc::fixtures::text_id(1);
1581 let mut scene = Scene::new(vec![fx::text(1, Scope::Root, "short", pos2(0.0, 0.0))]);
1582 let ctx = egui::Context::default();
1583 ctx.set_fonts(crate::canvas::build_fonts(
1584 blockworx_paint::FontChoice::default(),
1585 ));
1586 ctx.run_ui(egui::RawInput::default(), |_| {})
1588 .drop_without_applying_deltas();
1589
1590 assert!(
1594 scene
1595 .drawing()
1596 .presentation
1597 .text_extents
1598 .valid_for(id, "short")
1599 .is_none(),
1600 "precondition: no extent has been measured yet",
1601 );
1602 let short = frame(&ctx, &mut scene, id).expect("the frame measured the box it found");
1603
1604 scene.apply(vec![fx::text_content(
1606 1,
1607 "a much longer line of text that has to wrap onto several lines",
1608 )]);
1609 let grown = frame(&ctx, &mut scene, id).expect("the next frame measured the new text");
1610
1611 assert!(
1612 grown.h > short.h,
1613 "the box grew to fit the longer text ({short:?} -> {grown:?}) with no \
1614 edit cycle to trigger it",
1615 );
1616 }
1617
1618 fn tall_block(n: u32) -> blockworx_doc::opcode::OpCodes {
1621 fx::block_in(
1622 n,
1623 Scope::Root,
1624 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 150.0)),
1625 )
1626 }
1627
1628 fn slot(scene: &mut Scene, pin: PinId) -> PinSlot {
1629 scene
1630 .drawing()
1631 .held_pin(pin)
1632 .expect("the pin is in the document")
1633 .slot
1634 }
1635
1636 fn capacity(scene: &mut Scene, block: BlockId) -> u32 {
1639 slot_capacity(
1640 scene
1641 .drawing()
1642 .held_block(block)
1643 .expect("the block is in the document")
1644 .rect
1645 .size
1646 .h,
1647 )
1648 }
1649
1650 #[test]
1651 fn add_port_auto_named_adds_a_boundary_port() {
1652 let mut scene = Scene::new(vec![]);
1656 assert!(
1657 scene.drawing().ports_layer().next().is_none(),
1658 "precondition: the root scope starts with no ports"
1659 );
1660
1661 let rect = Rect::from_min_size(pos2(10.0, 10.0), vec2(4.0 * GRID_SIZE, GRID_SIZE));
1662 let id = scene.commit(|drawing| drawing.add_port_auto_named(rect));
1663
1664 let ports: Vec<PinId> = scene.drawing().ports_layer().map(|(_, _)| id).collect();
1665 assert_eq!(ports, vec![id], "exactly the stamped port is drawn");
1666 assert_eq!(
1667 scene
1668 .drawing()
1669 .held_pin(id)
1670 .expect("the port was inserted")
1671 .name,
1672 "Port 1",
1673 );
1674 }
1675
1676 #[test]
1679 fn editing_a_tag_keeps_the_route_anchors() {
1680 let (a, b, rid) = (pin_id(3), pin_id(4), route_id(5));
1681 let mut scene = Scene::new(vec![
1682 fx::block(1, 0.0),
1683 fx::block(2, 80.0),
1684 fx::pin(3, 1, DocPinSide::East, 0),
1685 fx::pin(4, 2, DocPinSide::West, 0),
1686 fx::route(5, Scope::Root, 3, 4, &[]),
1687 ]);
1688
1689 scene.commit(|drawing| drawing.set_pin_tag(a, "A1"));
1690
1691 let drawing = scene.drawing();
1692 assert_eq!(drawing.held_pin(a).expect("the pin survives").tag, "A1");
1693 let wire = drawing.auto_route(rid).expect("the wire survives");
1694 assert_eq!((wire.route.from, wire.route.to), (a, b));
1695 }
1696
1697 #[test]
1698 fn deleting_a_block_drops_its_whole_subtree() {
1699 let (parent, grandchild) = (block_id(1), block_id(2));
1700 let mut scene = Scene::new(vec![
1701 fx::block(1, 0.0),
1702 fx::block_in(
1703 2,
1704 Scope::Block(parent),
1705 Rect::from_min_max(pos2(8.0, 8.0), pos2(48.0, 48.0)),
1706 ),
1707 ]);
1708 assert!(
1709 scene.drawing().block(parent).is_some()
1710 && scene.drawing().held_block(grandchild).is_some(),
1711 "precondition: both blocks are in the document"
1712 );
1713
1714 scene.commit(|drawing| drawing.delete(Deletable::Shape(ShapeId::Rect(parent))));
1715
1716 let drawing = scene.drawing();
1717 assert!(
1718 drawing.held_block(parent).is_none(),
1719 "deleted block is gone"
1720 );
1721 assert!(
1722 drawing.held_block(grandchild).is_none(),
1723 "descendant subtree is gone too (no orphans)"
1724 );
1725 assert!(
1726 drawing.child_blocks().is_empty(),
1727 "the block is unlinked from the scope that held it"
1728 );
1729 }
1730
1731 #[test]
1734 fn nudge_pins_shifts_one_slot_and_clamps_at_the_edges() {
1735 let (a, pa) = (block_id(1), pin_id(3));
1736 let mut scene = Scene::new(vec![tall_block(1), fx::pin(3, 1, DocPinSide::East, 1)]);
1737 let max = capacity(&mut scene, a);
1738 assert!(max >= 2, "the tall block must hold at least slots 0..=2");
1739
1740 assert_eq!(slot(&mut scene, pa).offset, 1);
1741 scene.commit(|d| d.nudge_pins(&[pa], 1));
1742 assert_eq!(slot(&mut scene, pa).offset, 2);
1743 scene.commit(|d| d.nudge_pins(&[pa], -1));
1744 assert_eq!(slot(&mut scene, pa).offset, 1);
1745 scene.commit(|d| d.nudge_pins(&[pa], -1));
1747 assert_eq!(slot(&mut scene, pa).offset, 0);
1748 scene.commit(|d| d.nudge_pins(&[pa], -1));
1749 assert_eq!(slot(&mut scene, pa).offset, 0);
1750 scene.commit(|d| d.nudge_pins(&[pa], max as i32 + 5));
1752 assert_eq!(slot(&mut scene, pa).offset, max);
1753 scene.commit(|d| d.nudge_pins(&[pa], 1));
1754 assert_eq!(slot(&mut scene, pa).offset, max);
1755 }
1756
1757 #[test]
1760 fn nudge_pins_moves_a_group_rigidly_and_stops_at_a_member_boundary() {
1761 let (p0, p1) = (pin_id(3), pin_id(4));
1762 let mut scene = Scene::new(vec![
1763 tall_block(1),
1764 fx::pin(3, 1, DocPinSide::East, 0),
1765 fx::pin(4, 1, DocPinSide::East, 1),
1766 ]);
1767
1768 scene.commit(|d| d.nudge_pins(&[p0, p1], -1));
1770 assert_eq!(
1771 (slot(&mut scene, p0).offset, slot(&mut scene, p1).offset),
1772 (0, 1)
1773 );
1774 scene.commit(|d| d.nudge_pins(&[p0, p1], 1));
1776 assert_eq!(
1777 (slot(&mut scene, p0).offset, slot(&mut scene, p1).offset),
1778 (1, 2)
1779 );
1780 }
1781
1782 #[test]
1784 fn delete_shapes_removes_all_of_them() {
1785 let (a, b) = (block_id(1), block_id(2));
1786 let mut scene = Scene::new(vec![fx::block(1, 0.0), fx::block(2, 80.0)]);
1787
1788 scene.commit(|d| d.delete(Deletable::Shapes(vec![ShapeId::Rect(a), ShapeId::Rect(b)])));
1789
1790 let drawing = scene.drawing();
1791 assert!(drawing.block(a).is_none());
1792 assert!(drawing.block(b).is_none());
1793 }
1794
1795 #[test]
1796 fn delete_pins_drops_the_pins_and_their_routes() {
1797 let (pa, pb, rid) = (pin_id(3), pin_id(4), route_id(5));
1798 let mut scene = Scene::new(vec![
1799 fx::block(1, 0.0),
1800 fx::block(2, 120.0),
1801 fx::pin(3, 1, DocPinSide::East, 0),
1802 fx::pin(4, 2, DocPinSide::West, 0),
1803 fx::route(5, Scope::Root, 3, 4, &[]),
1804 ]);
1805 assert!(
1806 scene.drawing().auto_route(rid).is_some(),
1807 "precondition: the wire is in this scope"
1808 );
1809
1810 scene.commit(|d| d.delete(Deletable::Pins(vec![pa])));
1811
1812 let drawing = scene.drawing();
1813 assert!(drawing.held_pin(pa).is_none(), "the pin is gone");
1814 assert!(drawing.auto_route(rid).is_none(), "and the wire with it");
1815 assert!(drawing.held_pin(pb).is_some(), "the far endpoint survives");
1816 }
1817
1818 #[test]
1819 fn relocate_pins_moves_a_free_group_but_rejects_a_collision() {
1820 let (p0, p1, blocker) = (pin_id(3), pin_id(4), pin_id(5));
1822 let mut scene = Scene::new(vec![
1823 fx::block_in(
1824 1,
1825 Scope::Root,
1826 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 240.0)),
1827 ),
1828 fx::pin(3, 1, DocPinSide::East, 0),
1829 fx::pin(4, 1, DocPinSide::East, 1),
1830 fx::pin(5, 1, DocPinSide::East, 4),
1831 ]);
1832 let to = |pin, offset| PinMove {
1833 pin,
1834 to: PinSlot {
1835 side: DocPinSide::East,
1836 offset,
1837 },
1838 };
1839
1840 let ok = [to(p0, 2), to(p1, 3)];
1842 assert!(scene.drawing().can_relocate_pins(&ok));
1843 assert!(scene.commit(|d| d.relocate_pins(&ok)));
1844 assert_eq!(slot(&mut scene, p0).offset, 2);
1845 assert_eq!(slot(&mut scene, p1).offset, 3);
1846
1847 let bad = [to(p0, 3), to(p1, 4)];
1849 assert!(!scene.drawing().can_relocate_pins(&bad));
1850 assert!(!scene.commit(|d| d.relocate_pins(&bad)));
1851 assert_eq!(slot(&mut scene, p1).offset, 3);
1852 assert_eq!(slot(&mut scene, blocker).offset, 4);
1853 }
1854
1855 #[test]
1858 fn flip_block_vertical_mirrors_pin_slots_and_is_an_involution() {
1859 let (a, top, mid, low) = (block_id(1), pin_id(3), pin_id(4), pin_id(5));
1860 let mut scene = Scene::new(vec![
1861 tall_block(1),
1862 fx::pin(3, 1, DocPinSide::East, 0),
1863 fx::pin(4, 1, DocPinSide::West, 1),
1864 fx::pin(5, 1, DocPinSide::East, 2),
1865 ]);
1866 let h = capacity(&mut scene, a);
1867 assert!(h >= 2, "the tall block must hold at least slots 0..=2");
1868 let before = (
1869 slot(&mut scene, top),
1870 slot(&mut scene, mid),
1871 slot(&mut scene, low),
1872 );
1873
1874 scene.commit(|d| d.flip_block_vertical(a));
1875
1876 assert_eq!(slot(&mut scene, top).offset, h);
1878 assert_eq!(slot(&mut scene, mid).offset, h - 1);
1879 assert_eq!(slot(&mut scene, low).offset, h - 2);
1880 assert_eq!(slot(&mut scene, top).side, before.0.side);
1881 assert_eq!(slot(&mut scene, mid).side, before.1.side);
1882 assert_eq!(slot(&mut scene, low).side, before.2.side);
1883
1884 scene.commit(|d| d.flip_block_vertical(a));
1886 assert_eq!(slot(&mut scene, top).offset, before.0.offset);
1887 assert_eq!(slot(&mut scene, mid).offset, before.1.offset);
1888 assert_eq!(slot(&mut scene, low).offset, before.2.offset);
1889 }
1890
1891 #[test]
1894 fn pin_owner_locked_reads_the_pins_owning_block() {
1895 let mut scene = Scene::new(vec![
1896 fx::block(1, 0.0),
1897 fx::block(2, 80.0),
1898 fx::pin(3, 1, DocPinSide::East, 0),
1899 fx::pin(4, 2, DocPinSide::East, 0),
1900 fx::locked(1),
1901 ]);
1902 let drawing = scene.drawing();
1903 assert!(drawing.pin_owner_locked(pin_id(3)));
1904 assert!(!drawing.pin_owner_locked(pin_id(4)));
1905 assert!(!drawing.pin_owner_locked(pin_id(9)));
1907 }
1908
1909 #[test]
1910 fn shape_owner_locked_covers_blocks_and_ports() {
1911 let (c, g, port) = (block_id(1), block_id(2), pin_id(3));
1914 let mut scene = Scene::new(vec![
1915 fx::block(1, 0.0),
1916 fx::block_in(
1917 2,
1918 Scope::Block(c),
1919 Rect::from_min_max(pos2(60.0, 0.0), pos2(100.0, 40.0)),
1920 ),
1921 fx::pin_at(
1922 3,
1923 Scope::Block(c),
1924 "io",
1925 fx::slot(DocPinSide::West, 0),
1926 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
1927 ),
1928 fx::locked(1),
1929 ])
1930 .inside(c);
1931 let drawing = scene.drawing();
1932 assert!(drawing.shape_owner_locked(ShapeId::Port(port)));
1934 assert!(drawing.current_locked());
1935 assert!(!drawing.shape_owner_locked(ShapeId::Rect(g)));
1937 }
1938
1939 #[test]
1942 fn the_root_scope_is_never_locked() {
1943 let mut scene = Scene::new(vec![fx::pin_at(
1944 3,
1945 Scope::Root,
1946 "io",
1947 fx::slot(DocPinSide::West, 0),
1948 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
1949 )]);
1950 let drawing = scene.drawing();
1951 assert!(
1952 drawing.current().is_none(),
1953 "the root has no block entity, or this proves nothing"
1954 );
1955 assert!(!drawing.current_locked());
1956 assert!(!drawing.shape_owner_locked(ShapeId::Port(pin_id(3))));
1957 }
1958
1959 #[test]
1960 fn delete_pin_is_a_no_op_on_a_locked_block() {
1961 let pa = pin_id(3);
1962 let mut scene = Scene::new(vec![
1963 fx::block(1, 0.0),
1964 fx::pin(3, 1, DocPinSide::East, 0),
1965 fx::locked(1),
1966 ]);
1967 let before = scene.doc.stamp();
1968
1969 scene.commit(|d| d.delete(Deletable::Pins(vec![pa])));
1970
1971 assert_eq!(
1972 scene.doc.stamp(),
1973 before,
1974 "a refused delete authors nothing"
1975 );
1976 assert!(
1977 scene.drawing().held_pin(pa).is_some(),
1978 "locked block keeps its pin"
1979 );
1980 }
1981
1982 #[test]
1983 fn delete_port_is_a_no_op_on_a_locked_block() {
1984 let (c, pc) = (block_id(1), pin_id(3));
1987 let mut scene = Scene::new(vec![
1988 fx::block(1, 0.0),
1989 fx::pin_at(
1990 3,
1991 Scope::Block(c),
1992 "io",
1993 fx::slot(DocPinSide::West, 0),
1994 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
1995 ),
1996 fx::locked(1),
1997 ])
1998 .inside(c);
1999 let before = scene.doc.stamp();
2000
2001 scene.commit(|d| d.delete(Deletable::Shape(ShapeId::Port(pc))));
2002
2003 assert_eq!(
2004 scene.doc.stamp(),
2005 before,
2006 "a refused delete authors nothing"
2007 );
2008 assert!(
2009 scene.drawing().shape(ShapeId::Port(pc)).is_some(),
2010 "locked block keeps its port"
2011 );
2012 }
2013
2014 #[test]
2015 fn set_block_locked_flips_the_flag() {
2016 let a = block_id(1);
2017 let mut scene = Scene::new(vec![fx::block(1, 0.0)]);
2018 let locked = |scene: &mut Scene| {
2019 scene
2020 .drawing()
2021 .held_block(a)
2022 .expect("the block is in the document")
2023 .locked
2024 };
2025 assert!(!locked(&mut scene));
2026
2027 scene.commit(|d| d.set_block_locked(a, InterfaceLock::Locked));
2028 assert!(locked(&mut scene));
2029 scene.commit(|d| d.set_block_locked(a, InterfaceLock::Unlocked));
2030 assert!(!locked(&mut scene));
2031 }
2032
2033 #[test]
2038 fn a_locked_block_still_repositions_its_pins() {
2039 let pa = pin_id(3);
2040 let mut scene = Scene::new(vec![
2041 tall_block(1),
2042 fx::pin(3, 1, DocPinSide::East, 0),
2043 fx::locked(1),
2044 ]);
2045 assert_eq!(slot(&mut scene, pa).offset, 0);
2046 assert!(scene.drawing().pin_owner_locked(pa), "precondition: frozen");
2047
2048 scene.commit(|d| {
2049 d.move_pin_snapped(
2050 pa,
2051 PinSlot {
2052 side: DocPinSide::East,
2053 offset: 1,
2054 },
2055 );
2056 });
2057
2058 assert_eq!(
2059 slot(&mut scene, pa).offset,
2060 1,
2061 "moving a pin along its block is presentation, which a lock allows",
2062 );
2063 }
2064
2065 #[test]
2068 fn a_locked_block_refuses_its_pins_direction() {
2069 let pa = pin_id(3);
2070 let mut scene = Scene::new(vec![
2071 tall_block(1),
2072 fx::pin(3, 1, DocPinSide::East, 0),
2073 fx::locked(1),
2074 ]);
2075 assert!(scene.drawing().pin_owner_locked(pa), "precondition: frozen");
2076 let before = scene.doc.stamp();
2077
2078 scene.commit(|d| d.cycle_pin_kind(pa));
2079
2080 assert_eq!(
2081 scene.doc.stamp(),
2082 before,
2083 "I/O direction is what the pin means, and a lock protects it",
2084 );
2085 }
2086}