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::render::Course;
68use crate::theme::Style;
69use crate::{
70 edit::{
71 self,
72 geometry::PinMove,
73 naming::{InterfaceLock, LabelFitWidth, TagVisibility},
74 },
75 grid::{artwork_rect, pin_slot},
76 path::BlockPath,
77 shape::{BlockShape, Deletable, PinLocation, PortShape, RoleTarget, ShapeId, ShapeRef},
78 widget::{
79 auto_route::{Crossing, 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, Debug)]
95pub(crate) struct Conflicts {
96 pub overlaps: Vec<Rect>,
98 pub crossings: Vec<Rect>,
101}
102
103fn is_routing(id: ShapeId) -> bool {
105 matches!(id, ShapeId::Rect(_) | ShapeId::Port(_))
106}
107
108#[derive(Default)]
116pub(crate) struct Previewed {
117 shapes: Vec<(ShapeId, Rect)>,
118 routes: Vec<RouteId>,
119}
120
121pub struct Drawing<'a> {
125 base: IndexedDocument<'a>,
130 path: &'a BlockPath,
131 index: Option<&'a SpatialIndex>,
136 pub(super) presentation: &'a mut crate::presentation::Presentation,
141 gesture: &'a mut crate::gesture::Gesture,
145 previewed: Previewed,
146}
147
148#[derive(Clone, Copy, Debug)]
152pub struct LabelPlacement {
153 pub offset: f32,
154 pub side: Option<blockworx_doc::values::LabelSide>,
155}
156
157impl From<LabelPlacement> for edit::naming::LabelPlacement {
158 fn from(placement: LabelPlacement) -> Self {
159 edit::naming::LabelPlacement {
160 offset: placement.offset.into(),
161 side: placement.side,
162 }
163 }
164}
165
166pub(crate) fn reconstruct_scope(
186 indexed: &IndexedDocument<'_>,
187 presentation: &mut crate::presentation::Presentation,
188 path: &BlockPath,
189 reconstructing: crate::widget::routing::Reconstructing<'_>,
190) {
191 presentation.keep_routes(|id| indexed.doc.route(&id).is_some());
192 let mut discarded = crate::gesture::Gesture::idle();
193 Drawing::new(*indexed, path, presentation, &mut discarded)
194 .reconstruct_routes_reaching(reconstructing);
195 debug_assert!(
196 discarded.ops().is_empty(),
197 "reconstruction is a read; it must author nothing"
198 );
199}
200
201pub(crate) fn present_document(
202 indexed: &IndexedDocument<'_>,
203 presentation: &mut crate::presentation::Presentation,
204) {
205 presentation.keep_routes(|id| indexed.doc.route(&id).is_some());
209 let mut discarded = crate::gesture::Gesture::idle();
210 for path in wired_scopes(indexed) {
211 Drawing::new(*indexed, &path, presentation, &mut discarded).reconstruct_routes();
212 }
213 debug_assert!(
214 discarded.ops().is_empty(),
215 "reconstruction is a read; it must author nothing"
216 );
217}
218
219fn wired_scopes(indexed: &IndexedDocument<'_>) -> Vec<BlockPath> {
221 indexed
222 .index
223 .blocks
224 .iter()
225 .filter(|(_, entry)| !entry.routes.is_empty())
226 .map(|(&id, _)| {
227 let mut path = BlockPath::empty();
228 if let crate::path::Scope::Block(id) = crate::path::Scope::from_wire(id) {
229 path.push(id);
230 }
231 path
232 })
233 .collect()
234}
235
236const SETTLING_PASSES: usize = 8;
238
239pub fn settle_corners(
248 mut document: blockworx_doc::document::Document,
249) -> Result<blockworx_doc::document::Document, blockworx_doc::document::FoldError> {
250 for _ in 0..SETTLING_PASSES {
251 let Some(commit) = settling_pass(&document) else {
252 break;
253 };
254 document = document.try_apply(&commit)?;
255 }
256 Ok(document)
257}
258
259fn settling_pass(
262 document: &blockworx_doc::document::Document,
263) -> Option<blockworx_doc::commit::Commit> {
264 let mut index = blockworx_doc::document::DocIndex::of(document);
265 let indexed = index.view(document);
266 let mut presentation = crate::presentation::Presentation::default();
267 present_document(&indexed, &mut presentation);
268 let mut settled = blockworx_doc::commit::CommitBuilder::new("Settled every wire's corners");
269 for path in wired_scopes(&indexed) {
270 crate::widget::routing::settle_scope(&indexed, &path, &presentation, &mut settled);
271 }
272 settled.seal()
273}
274
275#[cfg(test)]
276mod settling {
277 use blockworx_doc::fixtures::scale::build_scale;
278
279 use super::{settle_corners, settling_pass};
280
281 #[test]
284 fn a_settled_document_is_a_fixed_point_of_the_solve() {
285 let generated = build_scale(3);
286 assert!(
287 generated
288 .routes()
289 .all(|(_, route)| route.waypoints.is_empty()),
290 "precondition: the grid is generated without corners",
291 );
292 let settled = settle_corners(generated).expect("the settling folds");
293 assert!(
294 settled
295 .routes()
296 .any(|(_, route)| !route.waypoints.is_empty())
297 );
298 let again = settling_pass(&settled);
299 assert!(
300 again.is_none(),
301 "the settled document still moves under the solve: {:?}",
302 again.map(|commit| commit
303 .ops()
304 .iter()
305 .map(|op| op.narrate())
306 .collect::<Vec<_>>()),
307 );
308 }
309}
310
311impl<'a> Drawing<'a> {
312 pub fn new(
313 base: IndexedDocument<'a>,
314 path: &'a BlockPath,
315 presentation: &'a mut crate::presentation::Presentation,
316 gesture: &'a mut crate::gesture::Gesture,
317 ) -> Self {
318 let view = gesture.view(base);
319 presentation.refresh_accents(&view);
320 presentation.refresh_routes(&view);
321 Self {
322 base,
323 path,
324 index: None,
325 presentation,
326 gesture,
327 previewed: Previewed::default(),
328 }
329 }
330
331 pub(super) fn indexed(&self) -> IndexedDocument<'_> {
335 self.gesture.view(self.base)
336 }
337
338 pub(super) fn split(
342 &mut self,
343 ) -> (IndexedDocument<'_>, &mut crate::presentation::Presentation) {
344 (self.gesture.view(self.base), self.presentation)
345 }
346
347 pub fn writability(&self) -> blockworx_store::doc::Writability {
351 self.gesture.writability()
352 }
353
354 pub fn authoring(&self) -> Authoring {
357 self.writability().into()
358 }
359
360 pub fn authoring_of(&self, shape: ShapeId) -> Authoring {
363 Authoring::of(self.writability(), self.shape_owner_locked(shape).into())
364 }
365
366 pub fn unlocked_scope(&self, block: BlockId) -> Option<edit::lock::UnlockedScope> {
370 edit::lock::UnlockedScope::of(&self.indexed(), crate::path::Scope::Block(block))
371 }
372
373 pub fn mint<K: IdKind>(&mut self) -> Id<K> {
377 self.gesture.mint(self.base.doc)
378 }
379
380 pub(super) fn ids(&self) -> Allocator {
383 self.gesture.ids(self.base.doc)
384 }
385
386 pub(super) fn author(
390 &mut self,
391 what: &'static str,
392 emit: impl FnOnce(&IndexedDocument<'_>, &mut CommitBuilder),
393 ) {
394 self.gesture.author(self.base, what, emit);
395 }
396
397 pub fn new_indexed(
400 base: IndexedDocument<'a>,
401 path: &'a BlockPath,
402 index: &'a SpatialIndex,
403 presentation: &'a mut crate::presentation::Presentation,
404 gesture: &'a mut crate::gesture::Gesture,
405 ) -> Self {
406 let view = gesture.view(base);
407 presentation.refresh_accents(&view);
408 presentation.refresh_routes(&view);
409 Self {
410 base,
411 path,
412 index: Some(index),
413 presentation,
414 gesture,
415 previewed: Previewed::default(),
416 }
417 }
418
419 pub fn current_scope(&self) -> crate::path::Scope {
422 self.path.scope()
423 }
424
425 pub(super) fn scope(&self) -> Option<&BlockIndex> {
428 self.indexed().index.scope(self.current_scope().wire_id())
429 }
430
431 pub(super) fn current(&self) -> Option<&Block> {
434 self.current_scope()
435 .block()
436 .and_then(|id| self.held_block(id))
437 }
438
439 pub(super) fn held_block(&self, id: BlockId) -> Option<&Block> {
440 self.indexed().doc.block(&id)
441 }
442
443 pub fn held_pin(&self, id: PinId) -> Option<&Pin> {
444 self.indexed().doc.pin(&id)
445 }
446
447 pub fn pin_shape(&self, pin: PinId) -> Option<ShapeId> {
451 let owner = self.held_pin(pin)?.owner;
452 Some(
453 if crate::path::Scope::from_wire(owner) == self.current_scope() {
454 ShapeId::Port(pin)
455 } else {
456 ShapeId::Rect(owner)
457 },
458 )
459 }
460
461 pub fn block_shape(&self, id: BlockId) -> Option<BlockShape<'_>> {
465 match self.shape(ShapeId::Rect(id))? {
466 ShapeRef::Block(block) => Some(block),
467 _ => None,
468 }
469 }
470
471 pub fn pin_on_shape(&self, pin: PinId) -> Option<(ShapeRef<'_>, &Pin)> {
475 Some((self.shape(self.pin_shape(pin)?)?, self.held_pin(pin)?))
476 }
477
478 pub fn pin_owner_locked(&self, pin: PinId) -> bool {
482 self.held_pin(pin)
483 .and_then(|pin| self.held_block(pin.owner))
484 .is_some_and(|owner| owner.locked)
485 }
486
487 pub fn current_locked(&self) -> bool {
490 self.current().is_some_and(|b| b.locked)
491 }
492
493 pub fn shape_owner_locked(&self, id: ShapeId) -> bool {
497 match id {
498 ShapeId::Rect(rid) => self.held_block(rid).is_some_and(|b| b.locked),
499 ShapeId::Port(_) => self.current_locked(),
500 ShapeId::Text(_) | ShapeId::Area(_) | ShapeId::Image(_) | ShapeId::Icon(_) => false,
501 }
502 }
503
504 pub fn shape_accents(&self) -> crate::presentation::ShapeAccents<'_> {
508 crate::presentation::ShapeAccents::new(self.current_scope(), &self.presentation.pin_accents)
509 }
510
511 fn ordered<'s, K, T>(
520 &'s self,
521 ids: impl IntoIterator<Item = Id<K>>,
522 lookup: impl Fn(&'s DocDocument, &Id<K>) -> Option<&'s T>,
523 ) -> Vec<(Id<K>, &'s T)>
524 where
525 K: IdKind + Ord + Copy,
526 T: Entity + 's,
527 {
528 let doc = self.indexed().doc;
529 let held = |id: &Id<K>| lookup(doc, id);
530 let entries: Vec<(Id<K>, &T)> = ids
531 .into_iter()
532 .filter_map(|id| Some((id, held(&id)?)))
533 .collect();
534 chronological(entries.iter().copied())
535 .into_iter()
536 .filter_map(|id| Some((id, held(&id)?)))
537 .collect()
538 }
539
540 pub fn child_blocks(&self) -> Vec<(BlockId, &Block)> {
542 crate::path::child_blocks(&self.indexed(), self.current_scope())
543 .into_iter()
544 .filter_map(|id| Some((id, self.held_block(id)?)))
545 .collect()
546 }
547
548 pub fn block(&self, id: BlockId) -> Option<&Block> {
551 self.scope()?
552 .children
553 .contains(&id)
554 .then(|| self.held_block(id))
555 .flatten()
556 }
557
558 pub(super) fn block_pins(&self, scope: crate::path::Scope) -> Vec<(PinId, &Pin)> {
562 let Some(entry) = self.indexed().index.scope(scope.wire_id()) else {
563 return Vec::new();
564 };
565 self.ordered(entry.pins.iter().copied(), |doc, id| doc.pin(id))
566 }
567
568 fn scope_texts(&self) -> Vec<(TextId, &Text)> {
569 let Some(scope) = self.scope() else {
570 return Vec::new();
571 };
572 self.ordered(scope.texts.iter().copied(), |doc, id| doc.text(id))
573 }
574
575 fn scope_areas(&self) -> Vec<(AreaId, &Area)> {
576 let Some(scope) = self.scope() else {
577 return Vec::new();
578 };
579 self.ordered(scope.areas.iter().copied(), |doc, id| doc.area(id))
580 }
581
582 fn scope_images(&self) -> Vec<(ImageId, &Image)> {
583 let Some(scope) = self.scope() else {
584 return Vec::new();
585 };
586 self.ordered(scope.images.iter().copied(), |doc, id| doc.image(id))
587 }
588
589 fn scope_owns(&self, owner: BlockId) -> bool {
593 self.scope().is_some() && crate::path::Scope::from_wire(owner) == self.current_scope()
594 }
595
596 fn scope_owned<'s, T: Entity>(
600 &'s self,
601 entity: Option<&'s T>,
602 owner: impl Fn(&T) -> BlockId,
603 ) -> Option<&'s T> {
604 entity.filter(|entity| self.scope_owns(owner(entity)))
605 }
606
607 fn asset(&self, hash: &AssetHash) -> Option<&Asset> {
610 self.indexed().doc.asset(hash)
611 }
612
613 pub fn delete(&mut self, what: Deletable) {
618 let targets: Vec<edit::delete::Target> = match what {
619 Deletable::Shape(id) => self.shape_targets(&[id]),
620 Deletable::Shapes(ids) => self.shape_targets(&ids),
621 Deletable::Route(rid) => vec![edit::delete::Target::Route(rid)],
622 Deletable::Pins(pins) => pins.into_iter().map(edit::delete::Target::Pin).collect(),
623 };
624 self.author("delete", |indexed, sink| {
625 edit::delete::selection(indexed, &targets, sink);
626 });
627 }
628
629 fn shape_targets(&mut self, shapes: &[ShapeId]) -> Vec<edit::delete::Target> {
633 for &id in shapes {
634 if let ShapeId::Icon(block) = id {
635 self.author("shape_targets", |indexed, sink| {
636 edit::assets::delete_icon(indexed.doc, block, sink);
637 });
638 }
639 }
640 shapes.iter().filter_map(|&id| delete_target(id)).collect()
641 }
642
643 #[tracing::instrument(level = "info", skip_all)]
649 pub(super) fn scope_route_ids(&self) -> Vec<RouteId> {
650 let Some(scope) = self.scope() else {
651 return Vec::new();
652 };
653 let doc = self.indexed().doc;
654 let held: Vec<(RouteId, &Route)> = scope
655 .routes
656 .iter()
657 .filter_map(|&id| Some((id, doc.route(&id)?)))
658 .collect();
659 chronological(held.iter().copied())
660 }
661
662 pub(super) fn route(&self, id: RouteId) -> Option<&Route> {
663 self.indexed().doc.route(&id)
664 }
665
666 pub fn auto_routes(&self) -> impl Iterator<Item = (RouteId, Wire<'_>)> {
667 self.scope_route_ids()
668 .into_iter()
669 .filter_map(|id| Some((id, self.auto_route(id)?)))
670 .collect::<Vec<_>>()
671 .into_iter()
672 }
673
674 pub fn auto_route(&self, id: RouteId) -> Option<Wire<'_>> {
678 let route = self
679 .route(id)
680 .filter(|route| self.scope_owns(route.owner))?;
681 Some(Wire {
682 route,
683 labels: route_labels(&self.indexed(), id),
684 })
685 }
686 pub fn add_route(
692 &mut self,
693 from: PinId,
694 to: crate::edit::create::RouteEnd,
695 waypoints: Vec<Waypoint>,
696 ) -> RouteId {
697 let id = self.mint();
698 let owner = self.current_scope();
699 self.author("add_route", |indexed, sink| {
700 edit::create::route(
701 indexed,
702 edit::create::NewRoute {
703 id,
704 owner,
705 from,
706 to,
707 waypoints,
708 },
709 sink,
710 );
711 });
712 id
713 }
714 pub fn route_geometry(&self, id: RouteId) -> Option<&RouteGeometry> {
717 self.presentation.routes.get(&id)
718 }
719
720 pub fn add_image(&mut self, placement: edit::assets::Placement, asset: &Asset) -> ShapeId {
725 let id: ImageId = self.mint();
726 let owner = self.current_scope();
727 self.author("add_image", |indexed, sink| {
728 edit::assets::image(
729 indexed.doc,
730 edit::assets::NewImage {
731 id,
732 owner,
733 placement,
734 },
735 asset,
736 sink,
737 );
738 });
739 ShapeId::Image(id)
740 }
741 #[cfg_attr(not(test), allow(dead_code))]
742 pub fn image(&self, id: ImageId) -> Option<&Image> {
743 self.scope_owned(self.indexed().doc.image(&id), |i| i.owner)
744 }
745
746 pub fn icon(&self, id: BlockId) -> Option<&Icon> {
751 self.block(id)
752 .and_then(|b| crate::edit::geometry::artwork(&b.icon))
753 }
754 pub fn set_icon(&mut self, id: BlockId, asset: &Asset) {
757 self.author("set_icon", |indexed, sink| {
758 edit::assets::set_icon(indexed.doc, id, asset, sink);
759 });
760 }
761
762 pub fn add_block(&mut self, start: Pos2, end: Pos2) -> BlockId {
765 let id = self.mint();
766 let scope = self.current_scope();
767 self.author("add_block", |indexed, sink| {
768 edit::create::block(
769 indexed.doc,
770 edit::create::NewBlock {
771 id,
772 scope,
773 start,
774 end,
775 },
776 sink,
777 );
778 });
779 id
780 }
781 pub fn add_rect_box(&mut self, start: Pos2, end: Pos2) -> BlockId {
783 self.add_block(start, end)
784 }
785
786 pub fn add_text_box(&mut self, pos: Pos2) -> TextId {
792 let id = self.mint();
793 let scope = self.current_scope();
794 self.author("add_text_box", |_indexed, sink| {
795 edit::create::text_box(id, scope, pos, sink);
796 });
797 id
798 }
799 pub fn text_box(&self, id: TextId) -> Option<&Text> {
800 self.scope_owned(self.indexed().doc.text(&id), |t| t.owner)
801 }
802
803 pub fn add_area(&mut self, start: Pos2, end: Pos2) -> ShapeId {
809 let id: AreaId = self.mint();
810 let scope = self.current_scope();
811 self.author("add_area", |_indexed, sink| {
812 edit::create::area(id, scope, start, end, sink);
813 });
814 ShapeId::Area(id)
815 }
816 pub fn areas(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
820 self.scope_areas()
821 .into_iter()
822 .map(|(id, c)| (ShapeId::Area(id), ShapeRef::Area(c)))
823 }
824
825 pub fn add_port_auto_named(&mut self, inner: Rect) -> PinId {
834 let id = self.mint();
835 let scope = self.current_scope();
836 self.author("add_port_auto_named", |indexed, sink| {
837 let Some(owner) = edit::lock::UnlockedScope::of(indexed, scope) else {
839 return;
840 };
841 edit::create::port(
842 indexed,
843 edit::create::NewPort {
844 id,
845 owner,
846 start: inner.min,
847 end: inner.max,
848 },
849 sink,
850 );
851 });
852 id
853 }
854
855 pub fn shape(&self, id: ShapeId) -> Option<ShapeRef<'_>> {
859 match id {
860 ShapeId::Rect(rid) => Some(ShapeRef::Block(self.shape_of_block(rid, self.block(rid)?))),
861 ShapeId::Port(pid) => {
864 let pin = self
865 .held_pin(pid)
866 .filter(|pin| self.scope_owns(pin.owner))?;
867 Some(ShapeRef::Port(PortShape { id: pid, pin }))
868 }
869 ShapeId::Text(tid) => {
870 let text = self.scope_owned(self.indexed().doc.text(&tid), |t| t.owner)?;
871 let extent = self.presentation.text_extents.valid_for(tid, text);
872 Some(ShapeRef::text(text, extent))
873 }
874 ShapeId::Area(cid) => self
875 .scope_owned(self.indexed().doc.area(&cid), |c| c.owner)
876 .map(ShapeRef::Area),
877 ShapeId::Image(sid) => {
878 let image = self.scope_owned(self.indexed().doc.image(&sid), |i| i.owner)?;
879 Some(ShapeRef::artwork(
880 artwork_rect(image.rect),
881 self.asset(&image.asset),
882 ))
883 }
884 ShapeId::Icon(rid) => {
885 let icon = crate::edit::geometry::artwork(&self.block(rid)?.icon)?;
886 Some(ShapeRef::artwork(
887 artwork_rect(icon.rect),
888 self.asset(&icon.asset),
889 ))
890 }
891 }
892 }
893 pub fn shape_tag_hidden(&self, id: ShapeId) -> Option<bool> {
897 match id {
898 ShapeId::Port(pid) => Some(self.shape(id)?.pin(pid)?.tag_hidden),
900 ShapeId::Rect(_)
901 | ShapeId::Text(_)
902 | ShapeId::Area(_)
903 | ShapeId::Image(_)
904 | ShapeId::Icon(_) => None,
905 }
906 }
907
908 pub fn set_shape_tag_hidden(&mut self, id: ShapeId, tag: TagVisibility) {
911 if let ShapeId::Port(pid) = id {
913 self.author("set_shape_tag_hidden", |indexed, sink| {
914 edit::naming::set_tag_visibility(indexed.doc, &[pid], tag, sink);
915 });
916 }
917 }
918
919 pub fn set_role(&mut self, target: RoleTarget, role: Option<u8>) {
922 let target = match target {
923 RoleTarget::Block(id) => edit::naming::AccentTarget::Block(id),
924 RoleTarget::Port(id) => edit::naming::AccentTarget::Port(id),
925 RoleTarget::Route(id) => edit::naming::AccentTarget::Route(id),
926 RoleTarget::Area(id) => edit::naming::AccentTarget::Area(id),
927 RoleTarget::Text(id) => edit::naming::AccentTarget::Text(id),
928 };
929 self.author("set_role", |indexed, sink| {
930 edit::naming::set_accent(indexed.doc, target, role, sink);
931 });
932 }
933
934 pub fn set_pins_kind(&mut self, pins: &[PinId], dir: PinDir) {
937 self.author("set_pins_kind", |indexed, sink| {
938 let targets = edit::lock::MaterialPin::all(indexed.doc, pins);
939 edit::naming::set_dirs(indexed.doc, &targets, dir, sink);
940 });
941 }
942
943 pub fn cycle_pin_kind(&mut self, pin: PinId) {
946 self.author("cycle_pin_kind", |indexed, sink| {
947 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
948 return;
949 };
950 edit::naming::cycle_dir(indexed.doc, target, sink);
951 });
952 }
953
954 pub fn set_pins_tag_hidden(&mut self, pins: &[PinId], tag: TagVisibility) {
956 self.author("set_pins_tag_hidden", |indexed, sink| {
957 edit::naming::set_tag_visibility(indexed.doc, pins, tag, sink);
958 });
959 }
960
961 pub fn set_block_locked(&mut self, block: BlockId, lock: InterfaceLock) {
963 self.author("set_block_locked", |indexed, sink| {
964 edit::naming::set_locked(indexed.doc, block, lock, sink);
965 });
966 }
967
968 pub fn set_title_text(&mut self, shape: ShapeId, text: &str) {
971 let Some(target) = title_target(shape) else {
972 return;
973 };
974 self.author("set_title_text", |indexed, sink| {
975 edit::naming::rename_title(indexed.doc, target, text, sink);
976 });
977 }
978
979 pub fn place_title(&mut self, shape: ShapeId, placement: LabelPlacement) {
981 let target = match shape {
982 ShapeId::Rect(id) => edit::naming::LabelTarget::BlockTitle(id),
983 ShapeId::Area(id) => edit::naming::LabelTarget::AreaTitle(id),
984 _ => return,
985 };
986 self.author("place_title", |indexed, sink| {
987 edit::naming::place_label(indexed.doc, target, placement.into(), sink);
988 });
989 }
990
991 pub fn set_type_label_text(&mut self, rect: BlockId, text: &str) {
993 self.author("set_type_label_text", |indexed, sink| {
994 edit::naming::rename_type(indexed.doc, rect, text, sink);
995 });
996 }
997
998 pub fn place_type_label(&mut self, rect: BlockId, placement: LabelPlacement) {
1000 self.author("place_type_label", |indexed, sink| {
1001 edit::naming::place_label(
1002 indexed.doc,
1003 edit::naming::LabelTarget::BlockType(rect),
1004 placement.into(),
1005 sink,
1006 );
1007 });
1008 }
1009
1010 pub fn label_fit(
1014 &self,
1015 pin: PinId,
1016 name: Option<&str>,
1017 type_name: Option<&str>,
1018 ) -> LabelFitWidth {
1019 let held = self.held_pin(pin);
1020 let name = name.unwrap_or_else(|| held.map_or("", |pin| &pin.name));
1021 let type_name = type_name.unwrap_or_else(|| held.map_or("", |pin| &pin.type_name));
1022 LabelFitWidth::new(crate::shape::port::width_for_labels(name, type_name))
1023 }
1024
1025 pub fn rename_pin(&mut self, pin: PinId, text: &str, fit: LabelFitWidth) {
1028 self.author("rename_pin", |indexed, sink| {
1029 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
1030 return;
1031 };
1032 edit::naming::rename_pin(indexed.doc, target, text, fit, sink);
1033 });
1034 }
1035
1036 pub fn set_pin_tag(&mut self, pin: PinId, text: &str) {
1039 self.author("set_pin_tag", |indexed, sink| {
1040 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
1041 return;
1042 };
1043 edit::naming::set_tag(indexed.doc, target, text, sink);
1044 });
1045 }
1046
1047 pub fn retype_pin(&mut self, pin: PinId, text: &str, fit: LabelFitWidth) {
1050 self.author("retype_pin", |indexed, sink| {
1051 let Some(target) = edit::lock::MaterialPin::of(indexed.doc, pin) else {
1052 return;
1053 };
1054 edit::naming::retype_pin(indexed.doc, target, text, fit, sink);
1055 });
1056 }
1057
1058 pub fn add_named_pin(&mut self, block: BlockId, loc: PinLocation) -> Option<PinId> {
1062 if edit::lock::UnlockedScope::of(&self.indexed(), crate::path::Scope::Block(block))
1065 .is_none()
1066 {
1067 tracing::debug!(target: "edit", edit = "add_named_pin", "declined");
1068 return None;
1069 }
1070 let id = self.mint();
1071 self.author("add_named_pin", |indexed, sink| {
1072 let Some(owner) =
1073 edit::lock::UnlockedScope::of(indexed, crate::path::Scope::Block(block))
1074 else {
1075 return;
1076 };
1077 edit::create::pin(
1078 indexed,
1079 edit::create::NewPin {
1080 id,
1081 owner,
1082 slot: slot_at(loc),
1083 },
1084 sink,
1085 );
1086 });
1087 Some(id)
1088 }
1089
1090 pub fn apply_resize(&mut self, shape: ShapeId, new_rect: Rect) {
1094 let target = match shape {
1095 ShapeId::Rect(id) => edit::geometry::ResizeTarget::Block(id),
1096 ShapeId::Port(id) => edit::geometry::ResizeTarget::Port(id),
1097 ShapeId::Area(id) => edit::geometry::ResizeTarget::Area(id),
1098 ShapeId::Image(id) => edit::geometry::ResizeTarget::Image(id),
1099 ShapeId::Icon(id) => edit::geometry::ResizeTarget::Icon(id),
1100 ShapeId::Text(id) => edit::geometry::ResizeTarget::Text(id),
1101 };
1102 self.author("apply_resize", |indexed, sink| {
1103 edit::geometry::resize(indexed, target, new_rect, sink);
1104 });
1105 }
1106
1107 pub fn move_pin_snapped(&mut self, pin: PinId, to: PinSlot) {
1112 self.author("move_pin_snapped", |indexed, sink| {
1113 edit::geometry::move_pin(indexed, pin, to, sink);
1114 });
1115 }
1116
1117 pub fn set_route_name(&mut self, route: RouteId, label: RouteLabelId, text: &str) {
1121 if text.trim().is_empty() {
1122 self.author("set_route_name", |indexed, sink| {
1123 edit::naming::clear_wire_label(indexed.doc, label, sink);
1124 });
1125 } else {
1126 self.author("set_route_name", |indexed, sink| {
1127 edit::naming::rename_route(indexed.doc, route, text, sink);
1128 });
1129 }
1130 }
1131
1132 pub fn set_text_box_content(&mut self, id: TextId, text: &str) {
1140 self.author("set_text_box_content", |indexed, sink| {
1141 edit::naming::edit_text(indexed.doc, id, text, sink);
1142 });
1143 }
1144
1145 pub fn refresh_text_extents(&mut self, painter: &Style<'_, impl Renderer>) {
1156 let stale: Vec<(TextId, Text)> = self
1157 .scope_texts()
1158 .into_iter()
1159 .filter(|(id, text)| {
1160 self.presentation
1161 .text_extents
1162 .valid_for(*id, text)
1163 .is_none()
1164 })
1165 .map(|(id, text)| (id, text.clone()))
1166 .collect();
1167 for (id, text) in stale {
1168 let width = crate::render::text_box::BoxWidth::of(&text);
1169 let size = crate::render::text_box::measure_box_size(painter, &text.text, width);
1170 self.presentation.text_extents.set(id, &text, size);
1171 }
1172 }
1173
1174 pub fn add_route_label(&mut self, route: RouteId, pos: Pos2) -> Option<RouteLabelId> {
1177 let distance = self.route_geometry(route)?.distance_along(pos);
1178 let id = self.mint();
1179 self.author("add_route_label", |indexed, sink| {
1180 edit::create::wire_label(indexed.doc, id, route, distance, sink);
1181 });
1182 Some(id)
1183 }
1184
1185 pub fn place_route_label(
1188 &mut self,
1189 label: RouteLabelId,
1190 dist: blockworx_doc::geometry::FracVal,
1191 ) {
1192 self.author("place_route_label", |indexed, sink| {
1193 edit::geometry::place_wire_label(indexed.doc, label, dist, sink);
1194 });
1195 }
1196
1197 pub fn flip_shape_pins(&mut self, id: ShapeId) {
1207 let target = match id {
1208 ShapeId::Rect(rid) => edit::geometry::FlipTarget::Block(rid),
1209 ShapeId::Port(pid) => edit::geometry::FlipTarget::Port(pid),
1210 ShapeId::Text(_) | ShapeId::Area(_) | ShapeId::Image(_) | ShapeId::Icon(_) => return,
1211 };
1212 self.author("flip_shape_pins", |indexed, sink| {
1213 edit::geometry::flip_pins(indexed, target, sink);
1214 });
1215 }
1216
1217 pub fn wrap_top(&mut self) {
1224 let id = self.mint();
1225 self.author("wrap_top", |indexed, sink| {
1226 edit::create::wrap_top(indexed, id, sink);
1227 });
1228 }
1229
1230 pub fn flip_block_vertical(&mut self, block: BlockId) {
1236 self.author("flip_block_vertical", |indexed, sink| {
1237 edit::geometry::flip_vertical(indexed, block, sink);
1238 });
1239 }
1240
1241 pub fn current_blocks(&self) -> impl Iterator<Item = (BlockId, &Block)> {
1244 self.child_blocks().into_iter()
1245 }
1246
1247 fn shape_of_block<'s>(&'s self, id: BlockId, block: &'s Block) -> BlockShape<'s> {
1251 BlockShape::new(
1252 block,
1253 self.block_pins(crate::path::Scope::Block(id)),
1254 crate::path::structure(&self.indexed(), id),
1255 )
1256 }
1257
1258 pub fn blocks_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1261 self.child_blocks().into_iter().map(|(id, block)| {
1262 (
1263 ShapeId::Rect(id),
1264 ShapeRef::Block(self.shape_of_block(id, block)),
1265 )
1266 })
1267 }
1268 pub fn ports_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1271 self.block_pins(self.current_scope())
1272 .into_iter()
1273 .map(|(id, pin)| (ShapeId::Port(id), ShapeRef::Port(PortShape { id, pin })))
1274 }
1275 pub fn texts_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1277 self.scope_texts().into_iter().map(|(id, text)| {
1278 let extent = self.presentation.text_extents.valid_for(id, text);
1279 (ShapeId::Text(id), ShapeRef::text(text, extent))
1280 })
1281 }
1282 pub fn images_layer(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1284 self.scope_images().into_iter().map(|(id, image)| {
1285 (
1286 ShapeId::Image(id),
1287 ShapeRef::artwork(artwork_rect(image.rect), self.asset(&image.asset)),
1288 )
1289 })
1290 }
1291 pub fn icons(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1295 self.child_blocks().into_iter().filter_map(|(id, block)| {
1296 let icon = crate::edit::geometry::artwork(&block.icon)?;
1297 Some((
1298 ShapeId::Icon(id),
1299 ShapeRef::artwork(artwork_rect(icon.rect), self.asset(&icon.asset)),
1300 ))
1301 })
1302 }
1303 pub fn shapes(&self) -> impl Iterator<Item = (ShapeId, ShapeRef<'_>)> {
1307 self.blocks_layer()
1308 .chain(self.ports_layer())
1309 .chain(self.texts_layer())
1310 .chain(self.images_layer())
1311 }
1312
1313 pub fn can_relocate_pins(&self, moves: &[PinMove]) -> bool {
1318 crate::edit::geometry::relocation_fits(&self.indexed(), moves)
1319 }
1320
1321 pub fn relocate_pins(&mut self, moves: &[PinMove]) -> bool {
1324 let applied = self.can_relocate_pins(moves);
1325 self.author("relocate_pins", |indexed, sink| {
1326 edit::geometry::relocate_pins(indexed, moves, sink);
1327 });
1328 applied
1329 }
1330
1331 pub fn nudge_pins(&mut self, pins: &[PinId], slot_delta: i32) {
1336 self.author("nudge_pins", |indexed, sink| {
1337 edit::geometry::nudge_pins(
1338 indexed,
1339 pins,
1340 edit::geometry::SlotDelta::new(slot_delta),
1341 sink,
1342 );
1343 });
1344 }
1345
1346 pub fn content_bounds(&self, painter: &Style<'_, impl Renderer>) -> Option<Rect> {
1352 let mut extent = blockworx_paint::Extent::measuring_through(painter.renderer());
1353 crate::widget::DrawingPasses::new(self).draw(&mut Style::new(painter.theme(), &mut extent));
1354 extent.finish()
1355 }
1356
1357 fn draw_key(id: ShapeId) -> (u8, ShapeId) {
1370 (shape_layer_rank(id), id)
1371 }
1372
1373 pub fn shape_candidates(&self, query: Rect) -> Vec<(ShapeId, ShapeRef<'_>)> {
1379 match self.index {
1380 Some(idx) => {
1381 let mut v: Vec<(ShapeId, ShapeRef<'_>)> = idx
1382 .in_rect(query)
1383 .filter_map(|hit| match hit {
1384 HitId::Shape(id) if !matches!(id, ShapeId::Area(_) | ShapeId::Icon(_)) => {
1390 self.shape(id).map(|s| (id, s))
1391 }
1392 _ => None,
1393 })
1394 .collect();
1395 v.sort_by_key(|(id, _)| Self::draw_key(*id));
1396 v
1397 }
1398 None => self.shapes().collect(),
1399 }
1400 }
1401
1402 pub fn hit_candidates(&self, query: Rect) -> Vec<(ShapeId, ShapeRef<'_>)> {
1407 let mut v = self.shape_candidates(query);
1408 v.reverse();
1409 v
1410 }
1411
1412 pub(super) fn preview_shapes(&mut self, previews: &[(ShapeId, Rect)]) {
1416 self.previewed.shapes.clear();
1417 for &(id, rect) in previews {
1418 self.previewed.shapes.push((id, rect));
1419 if let Some(icon) = id.block().map(ShapeId::Icon)
1420 && self.shape(icon).is_some()
1421 {
1422 self.previewed.shapes.push((icon, rect));
1423 }
1424 }
1425 }
1426
1427 pub(super) fn scope_path(&self) -> &BlockPath {
1430 self.path
1431 }
1432
1433 pub(super) fn preview_routes(&mut self, routes: Vec<RouteId>) {
1436 self.previewed.routes = routes;
1437 }
1438
1439 pub(crate) fn visible_ids(&self, viewport: Rect) -> Option<std::collections::HashSet<HitId>> {
1450 let idx = self.index?;
1451 let mut ids: std::collections::HashSet<HitId> = idx.in_rect(viewport).collect();
1452 for &(id, rect) in &self.previewed.shapes {
1453 if self.shape(id).is_some_and(|shape| {
1454 crate::render::bounds::shape_bounds_at(&shape, rect).intersects(viewport)
1455 }) {
1456 ids.insert(HitId::Shape(id));
1457 }
1458 }
1459 for &id in &self.previewed.routes {
1460 if self
1461 .auto_route(id)
1462 .zip(self.route_geometry(id))
1463 .is_some_and(|(wire, geometry)| {
1464 crate::render::bounds::route_bounds(&wire, geometry).intersects(viewport)
1465 })
1466 {
1467 ids.insert(HitId::Route(id));
1468 }
1469 }
1470 Some(ids)
1471 }
1472
1473 pub fn hops(&self, among: &[RouteId]) -> Hops {
1477 let _s = tracing::info_span!("hops", routes = among.len()).entered();
1478 let drawn: Vec<(RouteId, &RouteGeometry)> = among
1479 .iter()
1480 .filter_map(|&id| Some((id, self.route_geometry(id)?)))
1481 .collect();
1482 let hops = super::auto_route::route_hops(drawn.iter().map(|&(_, geometry)| geometry));
1483 let hops = drawn
1484 .into_iter()
1485 .zip(hops)
1486 .filter(|(_, hops)| !hops.is_empty())
1487 .map(|((id, _), hops)| (id, hops))
1488 .collect();
1489 Hops(hops)
1490 }
1491
1492 pub fn course<'h>(&'h self, id: RouteId, hops: &'h [Crossing]) -> Option<Course<'h>> {
1495 Some(Course {
1496 geometry: self.route_geometry(id)?,
1497 hops,
1498 })
1499 }
1500
1501 pub(crate) fn standing_conflicts(&self, shapes: &[ShapeId]) -> Conflicts {
1510 let built;
1511 let index = if let Some(index) = self.index {
1512 index
1513 } else {
1514 built = SpatialIndex::from_drawing(self);
1515 &built
1516 };
1517 let doc = self.indexed().doc;
1518 let cells = |id: ShapeId| {
1519 self.previewed
1520 .shapes
1521 .iter()
1522 .find(|&&(shape, _)| shape == id)
1523 .map(|&(_, rect)| crate::grid::grid_rect(rect.min, rect.max))
1524 .or_else(|| {
1525 crate::edit::geometry::placement(doc, crate::edit::geometry::Shape::from(id))
1526 .map(|(_, cells)| cells)
1527 })
1528 };
1529 let previewed = |hit: &HitId| match *hit {
1530 HitId::Shape(id) => self.previewed.shapes.iter().any(|&(shape, _)| shape == id),
1531 HitId::Route(id) => self.previewed.routes.contains(&id),
1532 };
1533 let from_preview = self
1534 .previewed
1535 .shapes
1536 .iter()
1537 .map(|&(id, _)| HitId::Shape(id))
1538 .chain(self.previewed.routes.iter().map(|&id| HitId::Route(id)));
1539 let asked: std::collections::HashSet<ShapeId> = shapes.iter().copied().collect();
1542 let mut conflicts = Conflicts::default();
1543 for &one in shapes {
1544 let Some(own) = cells(one) else {
1545 continue;
1546 };
1547 let rect = crate::grid::px_rect(own);
1548 let block = super::routing::obstacle_rect(rect);
1549 let near = index
1550 .in_rect(rect)
1551 .filter(|hit| !previewed(hit))
1552 .chain(from_preview.clone());
1553 for hit in near {
1554 match hit {
1555 HitId::Shape(other)
1556 if other != one
1557 && is_routing(other)
1558 && (other > one || !asked.contains(&other)) =>
1559 {
1560 let overlap = cells(other).and_then(|other| own.intersection(other));
1561 conflicts.overlaps.extend(overlap.map(crate::grid::px_rect));
1562 }
1563 HitId::Route(id) => {
1564 let Some(geometry) = self.route_geometry(id) else {
1565 continue;
1566 };
1567 conflicts.crossings.extend(
1568 geometry
1569 .iter_edges()
1570 .filter_map(|(_, edge)| block.edge_crossing(edge.start, edge.end))
1571 .map(|(from, to)| {
1572 let (from, to) = (
1573 crate::grid::px_point(from.into()),
1574 crate::grid::px_point(to.into()),
1575 );
1576 Rect::from_min_max(from.min(to), from.max(to))
1577 .expand(crate::grid::GRID_SIZE / 2.0)
1578 }),
1579 );
1580 }
1581 HitId::Shape(_) => {}
1582 }
1583 }
1584 }
1585 conflicts
1586 }
1587
1588 pub fn hops_of(&self, id: RouteId) -> Vec<Crossing> {
1591 let Some(bounds) = self
1592 .auto_route(id)
1593 .zip(self.route_geometry(id))
1594 .map(|(wire, geometry)| crate::render::bounds::route_bounds(&wire, geometry))
1595 else {
1596 return Vec::new();
1597 };
1598 let near: Vec<RouteId> = self
1599 .route_candidates(bounds)
1600 .into_iter()
1601 .map(|(id, _)| id)
1602 .collect();
1603 self.hops(&near).0.remove(&id).unwrap_or_default()
1604 }
1605
1606 pub(crate) fn route_candidates(&self, query: Rect) -> Vec<(RouteId, Wire<'_>)> {
1611 match self.index {
1612 Some(idx) => {
1613 let order = self.scope_route_ids();
1614 let mut v: Vec<(RouteId, Wire<'_>)> = idx
1615 .in_rect(query)
1616 .filter_map(|hit| match hit {
1617 HitId::Route(id) => self.auto_route(id).map(|r| (id, r)),
1618 HitId::Shape(_) => None,
1619 })
1620 .collect();
1621 v.sort_by_key(|(id, _)| {
1622 order
1623 .iter()
1624 .position(|other| other == id)
1625 .unwrap_or(usize::MAX)
1626 });
1627 v
1628 }
1629 None => self.auto_routes().collect(),
1630 }
1631 }
1632}
1633
1634pub struct Hops(ahash::HashMap<RouteId, Vec<Crossing>>);
1636
1637impl Hops {
1638 pub fn of(&self, id: RouteId) -> &[Crossing] {
1641 self.0.get(&id).map_or(&[], Vec::as_slice)
1642 }
1643}
1644
1645#[cfg(test)]
1646mod tests {
1647 use super::*;
1648 use crate::path::Scope;
1649 use crate::{
1650 edit::lower::slot_capacity,
1651 grid::GRID_SIZE,
1652 shape::Deletable,
1653 widget::test_fixtures::{self as fx, Scene},
1654 };
1655 use blockworx_doc::{
1656 fixtures::{block_id, pin_id, route_id},
1657 values::PinSide as DocPinSide,
1658 };
1659 use blockworx_geom::{pos2, vec2};
1660 use blockworx_paint::FontChoice;
1661 use blockworx_text::measure::Measured;
1662
1663 fn two_blocks(one: i32, other: i32) -> Scene {
1665 Scene::new(vec![
1666 fx::block_in(1, Scope::Root, fx::cells(one, 0, 4, 4)),
1667 fx::block_in(2, Scope::Root, fx::cells(other, 0, 4, 4)),
1668 ])
1669 }
1670
1671 fn both() -> [ShapeId; 2] {
1672 [ShapeId::Rect(block_id(1)), ShapeId::Rect(block_id(2))]
1673 }
1674
1675 #[test]
1678 fn an_overlap_is_marked_once_over_the_shared_cells() {
1679 let mut scene = two_blocks(0, 2);
1680 let conflicts = scene.drawing().standing_conflicts(&both());
1681 assert_eq!(conflicts.overlaps, vec![fx::cells(2, 0, 2, 4)]);
1682 }
1683
1684 #[test]
1688 fn a_dragged_block_is_marked_where_the_preview_draws_it() {
1689 let mut scene = two_blocks(0, 10);
1690 let mut drawing = scene.drawing();
1691 assert!(
1692 drawing.standing_conflicts(&both()).overlaps.is_empty(),
1693 "precondition: the committed blocks stand clear"
1694 );
1695 drawing.preview_shapes(&[(ShapeId::Rect(block_id(2)), fx::cells(3, 0, 4, 4))]);
1696 assert_eq!(
1697 drawing.standing_conflicts(&both()).overlaps,
1698 vec![fx::cells(3, 0, 1, 4)]
1699 );
1700
1701 let mut scene = two_blocks(0, 2);
1702 let mut drawing = scene.drawing();
1703 assert!(
1704 !drawing.standing_conflicts(&both()).overlaps.is_empty(),
1705 "precondition: the committed blocks overlap"
1706 );
1707 drawing.preview_shapes(&[(ShapeId::Rect(block_id(2)), fx::cells(10, 0, 4, 4))]);
1708 assert!(drawing.standing_conflicts(&both()).overlaps.is_empty());
1709 }
1710
1711 #[test]
1715 fn an_overlap_is_found_from_either_block() {
1716 let mut scene = two_blocks(0, 2);
1717 let drawing = scene.drawing();
1718 for one in both() {
1719 assert_eq!(
1720 drawing.standing_conflicts(&[one]).overlaps,
1721 vec![fx::cells(2, 0, 2, 4)],
1722 "asked about {one:?}"
1723 );
1724 }
1725 }
1726
1727 #[test]
1731 fn a_tool_mints_one_past_the_highest_id_the_document_holds() {
1732 let mut scene = Scene::new(vec![fx::block(7, 0.0), fx::pin(2, 7, DocPinSide::East, 0)]);
1733 assert!(
1734 scene.drawing().held_block(block_id(8)).is_none(),
1735 "precondition: b8 is free before the gesture",
1736 );
1737
1738 let (made, _) = scene.authored(|drawing| {
1739 (
1740 drawing.add_block(pos2(200.0, 0.0), pos2(240.0, 40.0)),
1741 drawing.add_text_box(pos2(300.0, 0.0)),
1742 )
1743 });
1744 assert_eq!(
1745 made,
1746 (block_id(8), blockworx_doc::fixtures::text_id(1)),
1747 "the block counts on from b7 while the texts start their own space at 1",
1748 );
1749
1750 scene.authored(|drawing| drawing.delete(Deletable::Shape(ShapeId::Rect(block_id(8)))));
1751 assert!(
1752 scene.drawing().held_block(block_id(8)).is_none(),
1753 "precondition: b8 is deleted",
1754 );
1755 let (again, _) =
1756 scene.authored(|drawing| drawing.add_block(pos2(0.0, 200.0), pos2(40.0, 240.0)));
1757 assert_eq!(
1758 again,
1759 block_id(9),
1760 "the marks never fall, so the next block cannot land on a departed id",
1761 );
1762 }
1763
1764 fn wire_outside_the_blocks() -> Scene {
1768 Scene::new(vec![
1769 fx::block(1, 0.0),
1770 fx::block(2, 120.0),
1771 fx::pin(3, 1, DocPinSide::East, 0),
1772 fx::pin(4, 2, DocPinSide::West, 0),
1773 fx::route(5, Scope::Root, 3, 4, &[(8, -30), (8, -30)]),
1774 ])
1775 }
1776
1777 #[test]
1781 fn the_content_bounds_hold_the_wires_too() {
1782 let mut scene = wire_outside_the_blocks();
1783 let theme = crate::theme::Theme::default();
1784 let canvas = Measured::new(FontChoice::default(), theme.palette().clone());
1785 let drawing = scene.drawing();
1786
1787 let blocks = drawing
1788 .blocks_layer()
1789 .map(|(_, shape)| shape.gui_rect())
1790 .reduce(Rect::union)
1791 .expect("the scene holds blocks");
1792 let wire = drawing
1793 .auto_routes()
1794 .filter_map(|(id, _)| drawing.route_geometry(id))
1795 .flat_map(|geometry| geometry.points())
1796 .fold(Rect::NOTHING, |acc, p| acc.union(Rect::from_min_max(p, p)));
1797 assert!(
1798 wire.is_positive() && !blocks.contains_rect(wire),
1799 "precondition: the wire must escape the blocks it joins \
1800 (wire {wire:?}, blocks {blocks:?})",
1801 );
1802
1803 let bounds = canvas
1804 .frame(|painter| drawing.content_bounds(&Style::new(&theme, painter)))
1805 .expect("the scene draws something");
1806 assert!(
1807 bounds.contains_rect(wire),
1808 "the fit cropped the wire: {bounds:?} does not hold {wire:?}",
1809 );
1810 }
1811
1812 #[test]
1821 fn a_gesture_reads_the_block_it_just_created() {
1822 let mut scene = Scene::new(vec![]);
1823 let (created, staged) = scene.commit(|drawing| {
1824 let created = drawing.add_rect_box(pos2(0.0, 0.0), pos2(40.0, 40.0));
1825 let seen = drawing
1826 .shape(ShapeId::Rect(created))
1827 .expect("the gesture's own block is in the document it reads");
1828 assert!(
1829 seen.title().is_some(),
1830 "including the title an editor would be seeded from",
1831 );
1832 (created, seen.gui_rect())
1833 });
1834 let settled = scene
1835 .drawing()
1836 .shape(ShapeId::Rect(created))
1837 .expect("the sealed commit folded")
1838 .gui_rect();
1839 assert_eq!(
1840 staged, settled,
1841 "the geometry read mid-gesture is the geometry the commit lands",
1842 );
1843 assert!(
1844 staged.area() > 0.0,
1845 "and it is a real rect, so the comparison is not two empties",
1846 );
1847 }
1848
1849 #[test]
1853 fn a_gesture_reads_nothing_it_did_not_write() {
1854 let mut scene = Scene::new(vec![tall_block(1)]);
1855 scene.commit(|drawing| {
1856 assert!(
1857 drawing.shape(ShapeId::Rect(block_id(1))).is_some(),
1858 "the scene's own block is there",
1859 );
1860 assert!(
1861 drawing.shape(ShapeId::Rect(block_id(2))).is_none(),
1862 "a block no one authored is not",
1863 );
1864 });
1865 }
1866
1867 #[test]
1875 fn a_text_boxs_extent_is_measured_for_whatever_text_it_holds() {
1876 fn frame(
1879 canvas: &Measured,
1880 scene: &mut Scene,
1881 id: TextId,
1882 ) -> Option<blockworx_doc::geometry::GridSize> {
1883 canvas.frame(|painter| {
1884 let theme = crate::theme::Theme::default();
1885 let style = crate::theme::Style::new(&theme, painter);
1886 let mut drawing = scene.drawing();
1887 drawing.refresh_text_extents(&style);
1888 let text = drawing.text_box(id).cloned()?;
1889 drawing.presentation.text_extents.valid_for(id, &text)
1890 })
1891 }
1892
1893 let id = blockworx_doc::fixtures::text_id(1);
1894 let mut scene = Scene::new(vec![fx::text(1, Scope::Root, "short", pos2(0.0, 0.0))]);
1895 let canvas = Measured::new(
1896 FontChoice::default(),
1897 crate::theme::Theme::default().palette().clone(),
1898 );
1899
1900 assert!(
1904 {
1905 let drawing = scene.drawing();
1906 let text = drawing
1907 .text_box(id)
1908 .cloned()
1909 .expect("the box is in the scene");
1910 drawing
1911 .presentation
1912 .text_extents
1913 .valid_for(id, &text)
1914 .is_none()
1915 },
1916 "precondition: no extent has been measured yet",
1917 );
1918 let short = frame(&canvas, &mut scene, id).expect("the frame measured the box it found");
1919
1920 scene.apply(vec![fx::text_content(
1922 1,
1923 "a much longer line of text than the box was measured for",
1924 )]);
1925 let grown = frame(&canvas, &mut scene, id).expect("the next frame measured the new text");
1926
1927 assert!(
1928 grown.w > short.w,
1929 "the box grew to fit the longer text ({short:?} -> {grown:?}) with no \
1930 edit cycle to trigger it",
1931 );
1932 }
1933
1934 fn tall_block(n: u32) -> blockworx_doc::opcode::OpCodes {
1937 fx::block_in(
1938 n,
1939 Scope::Root,
1940 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 150.0)),
1941 )
1942 }
1943
1944 fn slot(scene: &mut Scene, pin: PinId) -> PinSlot {
1945 scene
1946 .drawing()
1947 .held_pin(pin)
1948 .expect("the pin is in the document")
1949 .slot
1950 }
1951
1952 fn capacity(scene: &mut Scene, block: BlockId) -> u32 {
1955 slot_capacity(
1956 scene
1957 .drawing()
1958 .held_block(block)
1959 .expect("the block is in the document")
1960 .rect
1961 .size
1962 .h,
1963 )
1964 }
1965
1966 #[test]
1967 fn add_port_auto_named_adds_a_boundary_port() {
1968 let mut scene = Scene::new(vec![]);
1972 assert!(
1973 scene.drawing().ports_layer().next().is_none(),
1974 "precondition: the root scope starts with no ports"
1975 );
1976
1977 let rect = Rect::from_min_size(pos2(10.0, 10.0), vec2(4.0 * GRID_SIZE, GRID_SIZE));
1978 let id = scene.commit(|drawing| drawing.add_port_auto_named(rect));
1979
1980 let ports: Vec<PinId> = scene.drawing().ports_layer().map(|(_, _)| id).collect();
1981 assert_eq!(ports, vec![id], "exactly the stamped port is drawn");
1982 assert_eq!(
1983 scene
1984 .drawing()
1985 .held_pin(id)
1986 .expect("the port was inserted")
1987 .name,
1988 "Port 1",
1989 );
1990 }
1991
1992 #[test]
1995 fn editing_a_tag_keeps_the_route_anchors() {
1996 let (a, b, rid) = (pin_id(3), pin_id(4), route_id(5));
1997 let mut scene = Scene::new(vec![
1998 fx::block(1, 0.0),
1999 fx::block(2, 80.0),
2000 fx::pin(3, 1, DocPinSide::East, 0),
2001 fx::pin(4, 2, DocPinSide::West, 0),
2002 fx::route(5, Scope::Root, 3, 4, &[]),
2003 ]);
2004
2005 scene.commit(|drawing| drawing.set_pin_tag(a, "A1"));
2006
2007 let drawing = scene.drawing();
2008 assert_eq!(drawing.held_pin(a).expect("the pin survives").tag, "A1");
2009 let wire = drawing.auto_route(rid).expect("the wire survives");
2010 assert_eq!((wire.route.from, wire.route.to), (a, b));
2011 }
2012
2013 #[test]
2014 fn deleting_a_block_drops_its_whole_subtree() {
2015 let (parent, grandchild) = (block_id(1), block_id(2));
2016 let mut scene = Scene::new(vec![
2017 fx::block(1, 0.0),
2018 fx::block_in(
2019 2,
2020 Scope::Block(parent),
2021 Rect::from_min_max(pos2(8.0, 8.0), pos2(48.0, 48.0)),
2022 ),
2023 ]);
2024 assert!(
2025 scene.drawing().block(parent).is_some()
2026 && scene.drawing().held_block(grandchild).is_some(),
2027 "precondition: both blocks are in the document"
2028 );
2029
2030 scene.commit(|drawing| drawing.delete(Deletable::Shape(ShapeId::Rect(parent))));
2031
2032 let drawing = scene.drawing();
2033 assert!(
2034 drawing.held_block(parent).is_none(),
2035 "deleted block is gone"
2036 );
2037 assert!(
2038 drawing.held_block(grandchild).is_none(),
2039 "descendant subtree is gone too (no orphans)"
2040 );
2041 assert!(
2042 drawing.child_blocks().is_empty(),
2043 "the block is unlinked from the scope that held it"
2044 );
2045 }
2046
2047 #[test]
2050 fn nudge_pins_shifts_one_slot_and_clamps_at_the_edges() {
2051 let (a, pa) = (block_id(1), pin_id(3));
2052 let mut scene = Scene::new(vec![tall_block(1), fx::pin(3, 1, DocPinSide::East, 1)]);
2053 let max = capacity(&mut scene, a);
2054 assert!(max >= 2, "the tall block must hold at least slots 0..=2");
2055
2056 assert_eq!(slot(&mut scene, pa).offset, 1);
2057 scene.commit(|d| d.nudge_pins(&[pa], 1));
2058 assert_eq!(slot(&mut scene, pa).offset, 2);
2059 scene.commit(|d| d.nudge_pins(&[pa], -1));
2060 assert_eq!(slot(&mut scene, pa).offset, 1);
2061 scene.commit(|d| d.nudge_pins(&[pa], -1));
2063 assert_eq!(slot(&mut scene, pa).offset, 0);
2064 scene.commit(|d| d.nudge_pins(&[pa], -1));
2065 assert_eq!(slot(&mut scene, pa).offset, 0);
2066 scene.commit(|d| d.nudge_pins(&[pa], max as i32 + 5));
2068 assert_eq!(slot(&mut scene, pa).offset, max);
2069 scene.commit(|d| d.nudge_pins(&[pa], 1));
2070 assert_eq!(slot(&mut scene, pa).offset, max);
2071 }
2072
2073 #[test]
2076 fn nudge_pins_moves_a_group_rigidly_and_stops_at_a_member_boundary() {
2077 let (p0, p1) = (pin_id(3), pin_id(4));
2078 let mut scene = Scene::new(vec![
2079 tall_block(1),
2080 fx::pin(3, 1, DocPinSide::East, 0),
2081 fx::pin(4, 1, DocPinSide::East, 1),
2082 ]);
2083
2084 scene.commit(|d| d.nudge_pins(&[p0, p1], -1));
2086 assert_eq!(
2087 (slot(&mut scene, p0).offset, slot(&mut scene, p1).offset),
2088 (0, 1)
2089 );
2090 scene.commit(|d| d.nudge_pins(&[p0, p1], 1));
2092 assert_eq!(
2093 (slot(&mut scene, p0).offset, slot(&mut scene, p1).offset),
2094 (1, 2)
2095 );
2096 }
2097
2098 #[test]
2100 fn delete_shapes_removes_all_of_them() {
2101 let (a, b) = (block_id(1), block_id(2));
2102 let mut scene = Scene::new(vec![fx::block(1, 0.0), fx::block(2, 80.0)]);
2103
2104 scene.commit(|d| d.delete(Deletable::Shapes(vec![ShapeId::Rect(a), ShapeId::Rect(b)])));
2105
2106 let drawing = scene.drawing();
2107 assert!(drawing.block(a).is_none());
2108 assert!(drawing.block(b).is_none());
2109 }
2110
2111 #[test]
2112 fn delete_pins_drops_the_pins_and_their_routes() {
2113 let (pa, pb, rid) = (pin_id(3), pin_id(4), route_id(5));
2114 let mut scene = Scene::new(vec![
2115 fx::block(1, 0.0),
2116 fx::block(2, 120.0),
2117 fx::pin(3, 1, DocPinSide::East, 0),
2118 fx::pin(4, 2, DocPinSide::West, 0),
2119 fx::route(5, Scope::Root, 3, 4, &[]),
2120 ]);
2121 assert!(
2122 scene.drawing().auto_route(rid).is_some(),
2123 "precondition: the wire is in this scope"
2124 );
2125
2126 scene.commit(|d| d.delete(Deletable::Pins(vec![pa])));
2127
2128 let drawing = scene.drawing();
2129 assert!(drawing.held_pin(pa).is_none(), "the pin is gone");
2130 assert!(drawing.auto_route(rid).is_none(), "and the wire with it");
2131 assert!(drawing.held_pin(pb).is_some(), "the far endpoint survives");
2132 }
2133
2134 #[test]
2135 fn relocate_pins_moves_a_free_group_but_rejects_a_collision() {
2136 let (p0, p1, blocker) = (pin_id(3), pin_id(4), pin_id(5));
2138 let mut scene = Scene::new(vec![
2139 fx::block_in(
2140 1,
2141 Scope::Root,
2142 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 240.0)),
2143 ),
2144 fx::pin(3, 1, DocPinSide::East, 0),
2145 fx::pin(4, 1, DocPinSide::East, 1),
2146 fx::pin(5, 1, DocPinSide::East, 4),
2147 ]);
2148 let to = |pin, offset| PinMove {
2149 pin,
2150 to: PinSlot {
2151 side: DocPinSide::East,
2152 offset,
2153 },
2154 };
2155
2156 let ok = [to(p0, 2), to(p1, 3)];
2158 assert!(scene.drawing().can_relocate_pins(&ok));
2159 assert!(scene.commit(|d| d.relocate_pins(&ok)));
2160 assert_eq!(slot(&mut scene, p0).offset, 2);
2161 assert_eq!(slot(&mut scene, p1).offset, 3);
2162
2163 let bad = [to(p0, 3), to(p1, 4)];
2165 assert!(!scene.drawing().can_relocate_pins(&bad));
2166 assert!(!scene.commit(|d| d.relocate_pins(&bad)));
2167 assert_eq!(slot(&mut scene, p1).offset, 3);
2168 assert_eq!(slot(&mut scene, blocker).offset, 4);
2169 }
2170
2171 #[test]
2174 fn flip_block_vertical_mirrors_pin_slots_and_is_an_involution() {
2175 let (a, top, mid, low) = (block_id(1), pin_id(3), pin_id(4), pin_id(5));
2176 let mut scene = Scene::new(vec![
2177 tall_block(1),
2178 fx::pin(3, 1, DocPinSide::East, 0),
2179 fx::pin(4, 1, DocPinSide::West, 1),
2180 fx::pin(5, 1, DocPinSide::East, 2),
2181 ]);
2182 let h = capacity(&mut scene, a);
2183 assert!(h >= 2, "the tall block must hold at least slots 0..=2");
2184 let before = (
2185 slot(&mut scene, top),
2186 slot(&mut scene, mid),
2187 slot(&mut scene, low),
2188 );
2189
2190 scene.commit(|d| d.flip_block_vertical(a));
2191
2192 assert_eq!(slot(&mut scene, top).offset, h);
2194 assert_eq!(slot(&mut scene, mid).offset, h - 1);
2195 assert_eq!(slot(&mut scene, low).offset, h - 2);
2196 assert_eq!(slot(&mut scene, top).side, before.0.side);
2197 assert_eq!(slot(&mut scene, mid).side, before.1.side);
2198 assert_eq!(slot(&mut scene, low).side, before.2.side);
2199
2200 scene.commit(|d| d.flip_block_vertical(a));
2202 assert_eq!(slot(&mut scene, top).offset, before.0.offset);
2203 assert_eq!(slot(&mut scene, mid).offset, before.1.offset);
2204 assert_eq!(slot(&mut scene, low).offset, before.2.offset);
2205 }
2206
2207 #[test]
2210 fn pin_owner_locked_reads_the_pins_owning_block() {
2211 let mut scene = Scene::new(vec![
2212 fx::block(1, 0.0),
2213 fx::block(2, 80.0),
2214 fx::pin(3, 1, DocPinSide::East, 0),
2215 fx::pin(4, 2, DocPinSide::East, 0),
2216 fx::locked(1),
2217 ]);
2218 let drawing = scene.drawing();
2219 assert!(drawing.pin_owner_locked(pin_id(3)));
2220 assert!(!drawing.pin_owner_locked(pin_id(4)));
2221 assert!(!drawing.pin_owner_locked(pin_id(9)));
2223 }
2224
2225 #[test]
2226 fn shape_owner_locked_covers_blocks_and_ports() {
2227 let (c, g, port) = (block_id(1), block_id(2), pin_id(3));
2230 let mut scene = Scene::new(vec![
2231 fx::block(1, 0.0),
2232 fx::block_in(
2233 2,
2234 Scope::Block(c),
2235 Rect::from_min_max(pos2(60.0, 0.0), pos2(100.0, 40.0)),
2236 ),
2237 fx::pin_at(
2238 3,
2239 Scope::Block(c),
2240 "io",
2241 fx::slot(DocPinSide::West, 0),
2242 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
2243 ),
2244 fx::locked(1),
2245 ])
2246 .inside(c);
2247 let drawing = scene.drawing();
2248 assert!(drawing.shape_owner_locked(ShapeId::Port(port)));
2250 assert!(drawing.current_locked());
2251 assert!(!drawing.shape_owner_locked(ShapeId::Rect(g)));
2253 }
2254
2255 #[test]
2258 fn the_root_scope_is_never_locked() {
2259 let mut scene = Scene::new(vec![fx::pin_at(
2260 3,
2261 Scope::Root,
2262 "io",
2263 fx::slot(DocPinSide::West, 0),
2264 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
2265 )]);
2266 let drawing = scene.drawing();
2267 assert!(
2268 drawing.current().is_none(),
2269 "the root has no block entity, or this proves nothing"
2270 );
2271 assert!(!drawing.current_locked());
2272 assert!(!drawing.shape_owner_locked(ShapeId::Port(pin_id(3))));
2273 }
2274
2275 #[test]
2276 fn delete_pin_is_a_no_op_on_a_locked_block() {
2277 let pa = pin_id(3);
2278 let mut scene = Scene::new(vec![
2279 fx::block(1, 0.0),
2280 fx::pin(3, 1, DocPinSide::East, 0),
2281 fx::locked(1),
2282 ]);
2283 let before = scene.doc.stamp();
2284
2285 scene.commit(|d| d.delete(Deletable::Pins(vec![pa])));
2286
2287 assert_eq!(
2288 scene.doc.stamp(),
2289 before,
2290 "a refused delete authors nothing"
2291 );
2292 assert!(
2293 scene.drawing().held_pin(pa).is_some(),
2294 "locked block keeps its pin"
2295 );
2296 }
2297
2298 #[test]
2299 fn delete_port_is_a_no_op_on_a_locked_block() {
2300 let (c, pc) = (block_id(1), pin_id(3));
2303 let mut scene = Scene::new(vec![
2304 fx::block(1, 0.0),
2305 fx::pin_at(
2306 3,
2307 Scope::Block(c),
2308 "io",
2309 fx::slot(DocPinSide::West, 0),
2310 Rect::from_min_max(pos2(0.0, 0.0), pos2(40.0, 16.0)),
2311 ),
2312 fx::locked(1),
2313 ])
2314 .inside(c);
2315 let before = scene.doc.stamp();
2316
2317 scene.commit(|d| d.delete(Deletable::Shape(ShapeId::Port(pc))));
2318
2319 assert_eq!(
2320 scene.doc.stamp(),
2321 before,
2322 "a refused delete authors nothing"
2323 );
2324 assert!(
2325 scene.drawing().shape(ShapeId::Port(pc)).is_some(),
2326 "locked block keeps its port"
2327 );
2328 }
2329
2330 #[test]
2331 fn set_block_locked_flips_the_flag() {
2332 let a = block_id(1);
2333 let mut scene = Scene::new(vec![fx::block(1, 0.0)]);
2334 let locked = |scene: &mut Scene| {
2335 scene
2336 .drawing()
2337 .held_block(a)
2338 .expect("the block is in the document")
2339 .locked
2340 };
2341 assert!(!locked(&mut scene));
2342
2343 scene.commit(|d| d.set_block_locked(a, InterfaceLock::Locked));
2344 assert!(locked(&mut scene));
2345 scene.commit(|d| d.set_block_locked(a, InterfaceLock::Unlocked));
2346 assert!(!locked(&mut scene));
2347 }
2348
2349 #[test]
2352 fn a_locked_block_still_repositions_its_pins() {
2353 let pa = pin_id(3);
2354 let mut scene = Scene::new(vec![
2355 tall_block(1),
2356 fx::pin(3, 1, DocPinSide::East, 0),
2357 fx::locked(1),
2358 ]);
2359 assert_eq!(slot(&mut scene, pa).offset, 0);
2360 assert!(scene.drawing().pin_owner_locked(pa), "precondition: frozen");
2361
2362 scene.commit(|d| {
2363 d.move_pin_snapped(
2364 pa,
2365 PinSlot {
2366 side: DocPinSide::East,
2367 offset: 1,
2368 },
2369 );
2370 });
2371
2372 assert_eq!(
2373 slot(&mut scene, pa).offset,
2374 1,
2375 "moving a pin along its block is presentation, which a lock allows",
2376 );
2377 }
2378
2379 #[test]
2382 fn a_locked_block_refuses_its_pins_direction() {
2383 let pa = pin_id(3);
2384 let mut scene = Scene::new(vec![
2385 tall_block(1),
2386 fx::pin(3, 1, DocPinSide::East, 0),
2387 fx::locked(1),
2388 ]);
2389 assert!(scene.drawing().pin_owner_locked(pa), "precondition: frozen");
2390 let before = scene.doc.stamp();
2391
2392 scene.commit(|d| d.cycle_pin_kind(pa));
2393
2394 assert_eq!(
2395 scene.doc.stamp(),
2396 before,
2397 "I/O direction is what the pin means, and a lock protects it",
2398 );
2399 }
2400}