1use core::time::Duration;
16use std::sync::Arc;
17
18use ahash::{HashMap, HashSet};
19
20use blockworx_doc::block_model::{
21 AreaUpdate, Block, BlockInit, BlockUpdate, Icon, ImageUpdate, Label, LabelUpdate, Pin,
22 PinUpdate, RouteLabelUpdate, RouteUpdate, TextUpdate,
23};
24use blockworx_doc::commit::{Commit, CommitBuilder};
25use blockworx_doc::document::{DocIndex, Document, IndexedDocument, TitleBlockUpdate};
26use blockworx_doc::geometry::{
27 FracVal, GridPoint, GridRect, GridSize, PinSlot, ScreenRect, ScreenSize,
28};
29use blockworx_doc::id::{
30 AreaId, BlockId, EntityRef, ImageId, PinId, RouteId, RouteLabelId, TextId,
31};
32use blockworx_doc::opcode::{Crud, OpCodes};
33use blockworx_doc::values::{LabelSide, PinDir, PinSide, Role};
34
35use crate::edit::naming::{InterfaceLock, LabelVisibility, TagVisibility};
36use crate::grid::{GRID_SIZE, pin_slot_row};
37use crate::path::Scope;
38use crate::presentation::Presentation;
39use crate::progress::Progress;
40use crate::tools::names::ToolName;
41
42const APPEAR: Duration = Duration::from_millis(600);
44const MORPH: Duration = Duration::from_millis(500);
46const STAGGER: Duration = Duration::from_millis(120);
48
49pub struct Timeline {
58 label: String,
59 duration: Duration,
60 camera: Option<CameraPlan>,
61 tracks: Vec<Track>,
62 pantomime: Option<Pantomime>,
63}
64
65pub struct Pantomime {
73 pub tool: Option<ToolName>,
77 pub affordance: Affordance,
78 pub cursor: Vec<Keyframe>,
81}
82
83#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87pub enum Affordance {
88 Canvas,
90 Body(EntityRef),
92 Handle(EntityRef),
94 Stub(EntityRef),
97 Writing(EntityRef),
99}
100
101#[derive(Clone, Copy, PartialEq, Eq, Debug)]
105pub struct CameraPlan {
106 pub scope: Scope,
107 pub region: GridRect,
108}
109
110pub struct Track {
114 pub subject: EntityRef,
115 pub kind: TrackKind,
116 op: OpCodes,
117 pub keys: Vec<Keyframe>,
118}
119
120#[derive(Clone, Copy, PartialEq, Eq, Debug)]
121pub enum TrackKind {
122 Appear,
123 Vanish,
124 Morph,
125 Emphasis,
126 Overlay,
127 Pantomime,
128}
129
130impl TrackKind {
131 fn arrival(self) -> Easing {
135 match self {
136 TrackKind::Appear | TrackKind::Vanish | TrackKind::Morph => Easing::EaseOut,
137 TrackKind::Emphasis | TrackKind::Overlay | TrackKind::Pantomime => Easing::Linear,
138 }
139 }
140}
141
142#[derive(Clone, PartialEq, Debug)]
145pub struct Keyframe {
146 pub at: Duration,
147 pub value: TrackValue,
148 pub easing: Easing,
149}
150
151#[derive(Clone, PartialEq, Debug)]
162pub enum TrackValue {
163 Rect(GridRect),
164 Point(GridPoint),
165 Artwork(ScreenRect),
166 Path(Arc<[GridPoint]>),
169 Along {
170 route: RouteId,
171 at: FracVal,
172 },
173 Label {
178 slot: LabelSlot,
179 side: LabelSide,
180 offset: FracVal,
181 },
182 Text {
185 of: Written,
186 text: Arc<str>,
188 },
189 Flag {
193 at: Site,
194 state: FlagState,
195 },
196 Settled,
197}
198
199#[derive(Clone, Copy, PartialEq, Eq, Debug)]
202pub enum LabelSlot {
203 Title,
204 TypeLabel,
205}
206
207#[derive(Clone, Copy, PartialEq, Eq, Debug)]
213pub enum Site {
214 Shape(GridRect),
215 Anchor(GridPoint),
216 Wire,
219 Sheet,
223}
224
225impl Site {
226 fn bounds(self) -> Option<GridRect> {
227 match self {
228 Site::Shape(rect) => Some(rect),
229 Site::Anchor(at) => Some(spot(at)),
230 Site::Wire | Site::Sheet => None,
231 }
232 }
233}
234
235#[derive(Clone, Copy, PartialEq, Eq, Debug)]
239pub struct Written {
240 pub at: Site,
241 pub line: TextLine,
242}
243
244#[derive(Clone, Copy, PartialEq, Eq, Debug)]
248pub enum TextLine {
249 Label(LabelSlot),
250 Pin(PinLine),
251 Content,
253 WireName,
254 DocumentName,
255}
256
257#[derive(Clone, Copy, PartialEq, Eq, Debug)]
260pub enum PinLine {
261 Name,
262 Type,
263 Tag,
264}
265
266impl PinLine {
267 fn of(self, pin: &Pin) -> &str {
268 match self {
269 PinLine::Name => pin.name.as_ref(),
270 PinLine::Type => pin.type_name.as_ref(),
271 PinLine::Tag => pin.tag.as_ref(),
272 }
273 }
274}
275
276#[derive(Clone, Copy, PartialEq, Eq, Debug)]
280pub enum FlagState {
281 Accent(Role),
282 Lock(InterfaceLock),
283 Direction(PinDir),
284 Tag(TagVisibility),
285 Label(LabelSlot, LabelVisibility),
286}
287
288impl TrackValue {
289 fn bounds(&self) -> Option<GridRect> {
291 match self {
292 TrackValue::Rect(r) => Some(*r),
293 TrackValue::Point(p) => Some(spot(*p)),
294 TrackValue::Artwork(r) => Some(grid_bounds(*r)),
295 TrackValue::Path(points) => points.iter().map(|p| spot(*p)).reduce(union),
296 TrackValue::Text { of, .. } => of.at.bounds(),
297 TrackValue::Flag { at, .. } => at.bounds(),
298 TrackValue::Along { .. } | TrackValue::Label { .. } | TrackValue::Settled => None,
302 }
303 }
304}
305
306#[derive(Clone, Copy, PartialEq, Eq, Debug)]
307pub enum Easing {
308 Linear,
309 EaseOut,
310}
311
312impl Easing {
313 fn apply(self, p: Progress) -> Progress {
314 let t = f32::from(p);
315 match self {
316 Easing::Linear => p,
317 Easing::EaseOut => Progress::new(1.0 - (1.0 - t) * (1.0 - t)),
318 }
319 }
320}
321
322#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
325pub struct Playhead {
326 pub elapsed: Duration,
327}
328
329impl Playhead {
330 pub fn advance(&mut self, dt: Duration) {
331 self.elapsed = self.elapsed.saturating_add(dt);
332 }
333
334 pub fn done(self, timeline: &Timeline) -> bool {
335 self.elapsed >= timeline.duration
336 }
337}
338
339pub struct Frame {
343 pub tracks: Vec<FrameTrack>,
344 pub ghost: Option<Ghost>,
345}
346
347pub struct Ghost {
350 pub tool: Option<ToolName>,
351 pub affordance: Affordance,
352 pub at: [f32; 2],
355}
356
357pub struct FrameTrack {
358 pub subject: EntityRef,
359 pub kind: TrackKind,
360 pub value: FrameValue,
361 pub progress: Progress,
362}
363
364#[derive(Clone, PartialEq, Debug)]
368pub enum FrameValue {
369 Rect {
370 min: [f32; 2],
371 max: [f32; 2],
372 },
373 Point {
374 at: [f32; 2],
375 },
376 Artwork {
377 min: [f32; 2],
378 max: [f32; 2],
379 },
380 Path(Vec<[f32; 2]>),
381 Along {
382 route: RouteId,
383 at: f32,
384 },
385 Label {
386 slot: LabelSlot,
387 side: LabelSide,
388 offset: f32,
389 },
390 Text {
392 of: Written,
393 from: Arc<str>,
394 to: Arc<str>,
395 mix: Progress,
396 },
397 Flag {
399 at: Site,
400 state: FlagState,
401 },
402 Settled,
403}
404
405impl Timeline {
406 pub fn duration(&self) -> Duration {
407 self.duration
408 }
409
410 pub fn label(&self) -> &str {
411 &self.label
412 }
413
414 pub fn camera(&self) -> Option<CameraPlan> {
415 self.camera
416 }
417
418 pub fn tracks(&self) -> &[Track] {
419 &self.tracks
420 }
421
422 pub fn pantomime(&self) -> Option<&Pantomime> {
423 self.pantomime.as_ref()
424 }
425
426 #[cfg(test)]
431 pub(crate) fn undepicted(&self) -> Vec<&Track> {
432 self.tracks
433 .iter()
434 .filter(|track| !matches!(track.subject, EntityRef::Asset(_)))
435 .filter(|track| {
436 track
437 .keys
438 .iter()
439 .all(|key| key.value == TrackValue::Settled)
440 })
441 .collect()
442 }
443
444 pub fn recovered(&self) -> Option<Commit> {
448 let mut builder = CommitBuilder::new(self.label.clone());
449 for track in &self.tracks {
450 builder.push(track.op.clone());
451 }
452 builder.seal()
453 }
454
455 pub fn at(&self, t: Duration) -> Frame {
457 Frame {
458 tracks: self.tracks.iter().map(|track| track.at(t)).collect(),
459 ghost: self.pantomime.as_ref().map(|mime| mime.at(t)),
460 }
461 }
462
463 pub fn describe(&self) -> String {
466 let mut lines = vec![
467 format!("label: {}", self.label),
468 format!("duration: {}ms", ms(self.duration)),
469 ];
470 lines.push(match self.camera {
471 None => "camera: none".to_owned(),
472 Some(CameraPlan { scope, region }) => {
473 let scope = match scope {
474 Scope::Root => "root".to_owned(),
475 Scope::Block(id) => format!("block {id}"),
476 };
477 format!("camera: {scope} {}", rect(region))
478 }
479 });
480 for track in &self.tracks {
481 lines.push(format!("track {} {}", track.subject, kind_name(track.kind)));
482 keys(&mut lines, &track.keys);
483 }
484 match &self.pantomime {
485 None => lines.push("pantomime: none".to_owned()),
486 Some(mime) => {
487 lines.push(format!(
488 "pantomime: tool {}, {}",
489 match mime.tool {
490 None => "none".to_owned(),
491 Some(tool) => format!("{tool:?}"),
492 },
493 affordance_name(mime.affordance),
494 ));
495 keys(&mut lines, &mime.cursor);
496 }
497 }
498 lines.push(String::new());
499 lines.join("\n")
500 }
501}
502
503fn keys(lines: &mut Vec<String>, keys: &[Keyframe]) {
504 for key in keys {
505 lines.push(format!(
506 " {}ms {} {}",
507 ms(key.at),
508 easing_name(key.easing),
509 value(&key.value),
510 ));
511 }
512}
513
514fn affordance_name(affordance: Affordance) -> String {
515 match affordance {
516 Affordance::Canvas => "canvas".to_owned(),
517 Affordance::Body(at) => format!("body {at}"),
518 Affordance::Handle(at) => format!("handle {at}"),
519 Affordance::Stub(at) => format!("stub {at}"),
520 Affordance::Writing(at) => format!("writing {at}"),
521 }
522}
523
524fn ms(d: Duration) -> u128 {
525 d.as_millis()
526}
527
528fn rect(r: GridRect) -> String {
529 format!(
530 "[{} {} {}x{}]",
531 r.top_left.x, r.top_left.y, r.size.w, r.size.h
532 )
533}
534
535fn point(p: GridPoint) -> String {
536 format!("[{} {}]", p.x, p.y)
537}
538
539fn artwork(r: ScreenRect) -> String {
542 let (x, y) = (f32::from(r.top_left.x), f32::from(r.top_left.y));
543 let (w, h) = (f32::from(r.size.w), f32::from(r.size.h));
544 format!("[{x:.2} {y:.2} {w:.2}x{h:.2}]")
545}
546
547fn value(v: &TrackValue) -> String {
548 match v {
549 TrackValue::Rect(r) => format!("rect {}", rect(*r)),
550 TrackValue::Point(p) => format!("point {}", point(*p)),
551 TrackValue::Artwork(r) => format!("artwork {}", artwork(*r)),
552 TrackValue::Path(points) => {
553 let corners: Vec<String> = points.iter().map(|p| point(*p)).collect();
554 format!("path {}", corners.join(" -> "))
555 }
556 TrackValue::Along { route, at } => {
557 format!("along route {route} at {:.2}", f32::from(*at))
558 }
559 TrackValue::Label { slot, side, offset } => format!(
560 "label {} {} at {:.2}",
561 slot_name(*slot),
562 side_name(*side),
563 f32::from(*offset),
564 ),
565 TrackValue::Text { of, text } => {
566 format!("text {} {} {text:?}", site_name(of.at), line_name(of.line))
567 }
568 TrackValue::Flag { at, state } => {
569 format!("flag {} {}", site_name(*at), flag_name(*state))
570 }
571 TrackValue::Settled => "settled".to_owned(),
572 }
573}
574
575fn site_name(at: Site) -> String {
576 match at {
577 Site::Shape(r) => rect(r),
578 Site::Anchor(p) => point(p),
579 Site::Wire => "wire".to_owned(),
580 Site::Sheet => "sheet".to_owned(),
581 }
582}
583
584fn line_name(line: TextLine) -> &'static str {
585 match line {
586 TextLine::Label(slot) => slot_name(slot),
587 TextLine::Pin(PinLine::Name) => "pin-name",
588 TextLine::Pin(PinLine::Type) => "pin-type",
589 TextLine::Pin(PinLine::Tag) => "pin-tag",
590 TextLine::Content => "content",
591 TextLine::WireName => "wire-name",
592 TextLine::DocumentName => "document-name",
593 }
594}
595
596fn flag_name(state: FlagState) -> String {
597 match state {
598 FlagState::Accent(role) => format!("role {}", role_name(role)),
599 FlagState::Lock(InterfaceLock::Locked) => "lock locked".to_owned(),
600 FlagState::Lock(InterfaceLock::Unlocked) => "lock unlocked".to_owned(),
601 FlagState::Direction(dir) => format!("dir {}", dir_name(dir)),
602 FlagState::Tag(TagVisibility::Shown) => "tag shown".to_owned(),
603 FlagState::Tag(TagVisibility::Hidden) => "tag hidden".to_owned(),
604 FlagState::Label(slot, LabelVisibility::Shown) => {
605 format!("{} shown", slot_name(slot))
606 }
607 FlagState::Label(slot, LabelVisibility::Hidden) => {
608 format!("{} hidden", slot_name(slot))
609 }
610 }
611}
612
613fn role_name(role: Role) -> &'static str {
614 match role {
615 Role::Accent0 => "plain",
616 Role::Accent1 => "accent1",
617 Role::Accent2 => "accent2",
618 Role::Accent3 => "accent3",
619 Role::Accent4 => "accent4",
620 Role::Accent5 => "accent5",
621 Role::Accent6 => "accent6",
622 Role::Accent7 => "accent7",
623 Role::Accent8 => "accent8",
624 }
625}
626
627fn dir_name(dir: PinDir) -> &'static str {
628 match dir {
629 PinDir::Input => "input",
630 PinDir::Output => "output",
631 PinDir::InOut => "in-out",
632 }
633}
634
635fn slot_name(slot: LabelSlot) -> &'static str {
636 match slot {
637 LabelSlot::Title => "title",
638 LabelSlot::TypeLabel => "type",
639 }
640}
641
642fn side_name(side: LabelSide) -> &'static str {
643 match side {
644 LabelSide::Top => "top",
645 LabelSide::Center => "center",
646 LabelSide::Bottom => "bottom",
647 }
648}
649
650fn kind_name(kind: TrackKind) -> &'static str {
651 match kind {
652 TrackKind::Appear => "appear",
653 TrackKind::Vanish => "vanish",
654 TrackKind::Morph => "morph",
655 TrackKind::Emphasis => "emphasis",
656 TrackKind::Overlay => "overlay",
657 TrackKind::Pantomime => "pantomime",
658 }
659}
660
661fn easing_name(e: Easing) -> &'static str {
662 match e {
663 Easing::Linear => "linear",
664 Easing::EaseOut => "ease-out",
665 }
666}
667
668impl Track {
669 fn end(&self) -> Duration {
670 ends(&self.keys).1
671 }
672
673 #[cfg(test)]
675 fn start(&self) -> Duration {
676 ends(&self.keys).0
677 }
678
679 fn at(&self, t: Duration) -> FrameTrack {
681 let (start, end) = ends(&self.keys);
682 FrameTrack {
683 subject: self.subject,
684 kind: self.kind,
685 value: sample(&self.keys, t),
686 progress: Progress::through(t.saturating_sub(start), end.saturating_sub(start)),
687 }
688 }
689}
690
691fn ends(keys: &[Keyframe]) -> (Duration, Duration) {
695 let end = keys.last().map_or(Duration::ZERO, |k| k.at);
696 match keys {
697 [first, _, ..] => (first.at, end),
698 _ => (Duration::ZERO, end),
699 }
700}
701
702fn sample(keys: &[Keyframe], t: Duration) -> FrameValue {
706 match keys {
707 [] => FrameValue::Settled,
708 [only] => frame_value(&only.value),
709 keys => match keys.iter().position(|k| k.at > t) {
710 None => frame_value(&keys[keys.len() - 1].value),
711 Some(0) => frame_value(&keys[0].value),
712 Some(i) => {
713 let (from, to) = (&keys[i - 1], &keys[i]);
714 let local =
715 Progress::through(t.saturating_sub(from.at), to.at.saturating_sub(from.at));
716 lerp(&from.value, &to.value, to.easing.apply(local))
717 }
718 },
719 }
720}
721
722fn cell(v: i32) -> f32 {
723 v as f32
724}
725
726fn corners(r: GridRect) -> ([f32; 2], [f32; 2]) {
727 (
728 [cell(r.left()), cell(r.top())],
729 [cell(r.right()), cell(r.bottom())],
730 )
731}
732
733fn px_corners(r: ScreenRect) -> ([f32; 2], [f32; 2]) {
734 let min = [f32::from(r.top_left.x), f32::from(r.top_left.y)];
735 (
736 min,
737 [min[0] + f32::from(r.size.w), min[1] + f32::from(r.size.h)],
738 )
739}
740
741fn frame_value(v: &TrackValue) -> FrameValue {
742 match v {
743 TrackValue::Settled => FrameValue::Settled,
744 TrackValue::Rect(r) => {
745 let (min, max) = corners(*r);
746 FrameValue::Rect { min, max }
747 }
748 TrackValue::Point(p) => FrameValue::Point {
749 at: [cell(p.x), cell(p.y)],
750 },
751 TrackValue::Artwork(r) => {
752 let (min, max) = px_corners(*r);
753 FrameValue::Artwork { min, max }
754 }
755 TrackValue::Path(points) => {
756 FrameValue::Path(points.iter().map(|p| [cell(p.x), cell(p.y)]).collect())
757 }
758 TrackValue::Along { route, at } => FrameValue::Along {
759 route: *route,
760 at: f32::from(*at),
761 },
762 TrackValue::Label { slot, side, offset } => FrameValue::Label {
763 slot: *slot,
764 side: *side,
765 offset: f32::from(*offset),
766 },
767 TrackValue::Text { of, text } => FrameValue::Text {
770 of: *of,
771 from: text.clone(),
772 to: text.clone(),
773 mix: Progress::one(),
774 },
775 TrackValue::Flag { at, state } => FrameValue::Flag {
776 at: *at,
777 state: *state,
778 },
779 }
780}
781
782fn lerp(from: &TrackValue, to: &TrackValue, p: Progress) -> FrameValue {
783 let t = f32::from(p);
784 let mix = |a: [f32; 2], b: [f32; 2]| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
785 match (frame_value(from), frame_value(to)) {
786 (FrameValue::Rect { min: a0, max: a1 }, FrameValue::Rect { min: b0, max: b1 }) => {
787 FrameValue::Rect {
788 min: mix(a0, b0),
789 max: mix(a1, b1),
790 }
791 }
792 (FrameValue::Artwork { min: a0, max: a1 }, FrameValue::Artwork { min: b0, max: b1 }) => {
793 FrameValue::Artwork {
794 min: mix(a0, b0),
795 max: mix(a1, b1),
796 }
797 }
798 (FrameValue::Point { at: a }, FrameValue::Point { at: b }) => {
799 FrameValue::Point { at: mix(a, b) }
800 }
801 (FrameValue::Path(from), FrameValue::Path(to)) => FrameValue::Path(wipe(&from, &to, t)),
802 (FrameValue::Along { route: a, at: x }, FrameValue::Along { route: b, at: y })
803 if a == b =>
804 {
805 FrameValue::Along {
806 route: a,
807 at: x + (y - x) * t,
808 }
809 }
810 (
813 FrameValue::Label { offset: x, .. },
814 FrameValue::Label {
815 slot,
816 side,
817 offset: y,
818 },
819 ) => FrameValue::Label {
820 slot,
821 side,
822 offset: x + (y - x) * t,
823 },
824 (FrameValue::Text { from, .. }, FrameValue::Text { of, to: now, .. }) => FrameValue::Text {
827 of,
828 from,
829 to: now,
830 mix: p,
831 },
832 (FrameValue::Flag { state: was, .. }, FrameValue::Flag { at, state: now }) => {
835 FrameValue::Flag {
836 at,
837 state: if p.is_complete() { now } else { was },
838 }
839 }
840 (_, settled) => settled,
842 }
843}
844
845fn wipe(from: &[[f32; 2]], to: &[[f32; 2]], t: f32) -> Vec<[f32; 2]> {
854 if to.len() < 2 && from.len() > 1 {
855 reveal(from, 1.0 - t)
856 } else {
857 reveal(to, t)
858 }
859}
860
861fn reveal(points: &[[f32; 2]], t: f32) -> Vec<[f32; 2]> {
864 if t >= 1.0 {
865 return points.to_vec();
866 }
867 let leg = |a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
868 let Some(&start) = points.first() else {
869 return Vec::new();
870 };
871 let mut drawn = vec![start];
872 let mut left = points
873 .windows(2)
874 .map(|pair| leg(pair[0], pair[1]))
875 .sum::<f32>()
876 * t.max(0.0);
877 for pair in points.windows(2) {
878 let length = leg(pair[0], pair[1]);
879 if length <= left {
880 drawn.push(pair[1]);
881 left -= length;
882 continue;
883 }
884 if left > 0.0 {
885 let f = left / length;
886 drawn.push([
887 pair[0][0] + (pair[1][0] - pair[0][0]) * f,
888 pair[0][1] + (pair[1][1] - pair[0][1]) * f,
889 ]);
890 }
891 break;
892 }
893 drawn
894}
895
896impl Pantomime {
897 fn at(&self, t: Duration) -> Ghost {
899 Ghost {
900 tool: self.tool,
901 affordance: self.affordance,
902 at: match sample(&self.cursor, t) {
903 FrameValue::Point { at } => at,
904 _ => [0.0, 0.0],
907 },
908 }
909 }
910}
911
912fn pantomime(scene: &Scene<'_>, label: &str, tracks: &[Track]) -> Option<Pantomime> {
925 let verb = Verb::opening(label)?;
926 let depicted = || {
927 tracks
928 .iter()
929 .filter_map(|track| Some((track, track_region(scene, track)?)))
930 };
931 let (track, region) = depicted()
935 .find(|(track, _)| verb.armed(track).is_some())
936 .or_else(|| depicted().next())?;
937 Some(Pantomime {
938 tool: verb.armed(track),
939 affordance: affordance(scene, track),
940 cursor: cursor(track, region),
941 })
942}
943
944#[derive(Clone, Copy, PartialEq, Eq, Debug)]
952enum Verb {
953 Add,
954 Create,
955 Edit,
956 Modify,
957 Move,
958 Rename,
959 Resize,
960 Retype,
961}
962
963impl Verb {
964 fn opening(label: &str) -> Option<Self> {
965 let word = label.split_whitespace().next()?.to_ascii_lowercase();
966 Some(match word.as_str() {
967 "add" => Verb::Add,
968 "create" => Verb::Create,
969 "edit" => Verb::Edit,
970 "modify" => Verb::Modify,
971 "move" | "nudge" => Verb::Move,
974 "rename" | "name" => Verb::Rename,
977 "resize" => Verb::Resize,
978 "retype" => Verb::Retype,
979 _ => return None,
980 })
981 }
982
983 fn armed(self, track: &Track) -> Option<ToolName> {
987 use EntityRef as E;
988 let artwork = track
989 .keys
990 .iter()
991 .any(|key| matches!(key.value, TrackValue::Artwork(_)));
992 Some(match (self, track.subject) {
993 (Verb::Add, E::Block(_)) if artwork => ToolName::Icon,
995 (Verb::Add, E::Block(_)) => ToolName::NewBlock,
996 (Verb::Add, E::Area(_)) => ToolName::NewArea,
997 (Verb::Add, E::Pin(_)) => ToolName::AddPort,
998 (Verb::Add, E::Image(_)) => ToolName::NewImage,
999 (Verb::Add, E::Text(_)) => ToolName::AddText,
1000 (Verb::Add, E::RouteLabel(_)) => ToolName::AddRouteLabel,
1001 (Verb::Create, E::Route(_)) => ToolName::Route,
1002 (Verb::Move, E::Block(_)) => match written_slot(track) {
1003 Some(LabelSlot::Title) => ToolName::MoveTitle,
1004 Some(LabelSlot::TypeLabel) => ToolName::MoveBlockType,
1005 None => ToolName::MoveBlock,
1008 },
1009 (Verb::Move, E::Area(_) | E::Text(_) | E::Image(_)) => ToolName::MoveBlock,
1010 (Verb::Move, E::Pin(_)) => ToolName::MovePin,
1011 (Verb::Move, E::RouteLabel(_)) => ToolName::MoveLabel,
1012 (Verb::Resize, E::Block(_) | E::Area(_) | E::Image(_)) => ToolName::ResizeBlock,
1013 (Verb::Modify, E::Route(_)) => ToolName::EditRoute,
1014 (Verb::Rename, E::Pin(_)) => ToolName::RenamePin,
1015 (Verb::Rename, E::Route(_)) => ToolName::RenameRoute,
1016 (Verb::Rename, E::Block(_) | E::Area(_)) => ToolName::RenameTitle,
1017 (Verb::Retype, E::Pin(_)) => ToolName::RetypePin,
1018 (Verb::Retype, E::Block(_)) => ToolName::RenameBlockType,
1019 (Verb::Edit, E::Text(_)) => ToolName::EditTextBox,
1020 _ => return None,
1023 })
1024 }
1025}
1026
1027fn written_slot(track: &Track) -> Option<LabelSlot> {
1030 track.keys.iter().find_map(|key| match key.value {
1031 TrackValue::Label { slot, .. }
1032 | TrackValue::Text {
1033 of:
1034 Written {
1035 line: TextLine::Label(slot),
1036 ..
1037 },
1038 ..
1039 } => Some(slot),
1040 _ => None,
1041 })
1042}
1043
1044fn affordance(scene: &Scene<'_>, track: &Track) -> Affordance {
1051 let subject = track.subject;
1052 let holds = |of: fn(&TrackValue) -> bool| track.keys.iter().any(|key| of(&key.value));
1053 if holds(|value| {
1054 matches!(
1055 value,
1056 TrackValue::Text { .. } | TrackValue::Along { .. } | TrackValue::Label { .. }
1057 )
1058 }) {
1059 return Affordance::Writing(subject);
1060 }
1061 if matches!(subject, EntityRef::Pin(_)) && !holds(|value| matches!(value, TrackValue::Rect(_)))
1062 {
1063 return Affordance::Stub(subject);
1064 }
1065 match track.kind {
1066 TrackKind::Appear if !scene.held(subject) => Affordance::Canvas,
1067 TrackKind::Morph if resizes(track) => Affordance::Handle(subject),
1068 _ => Affordance::Body(subject),
1069 }
1070}
1071
1072fn resizes(track: &Track) -> bool {
1075 let extent = |key: &Keyframe| key.value.bounds().map(|r| r.size);
1076 match (track.keys.first(), track.keys.last()) {
1077 (Some(first), Some(last)) => extent(first) != extent(last),
1078 _ => false,
1079 }
1080}
1081
1082fn cursor(track: &Track, region: GridRect) -> Vec<Keyframe> {
1094 let (start, end) = ends(&track.keys);
1095 let bounds = |key: &Keyframe| key.value.bounds().unwrap_or(region);
1096 let (first, last) = match (track.keys.first(), track.keys.last()) {
1097 (Some(first), Some(last)) => (bounds(first), bounds(last)),
1098 _ => (region, region),
1099 };
1100 let reached = start + end.saturating_sub(start) / 2;
1101 let hold = |at: GridPoint| {
1102 vec![
1103 step(start, entry(region, at)),
1104 step(reached, at),
1105 step(end, at),
1106 ]
1107 };
1108 match track.kind {
1109 TrackKind::Appear => {
1110 let seed = first.top_left;
1111 let far = track.keys.last().map_or(region.top_left, |key| {
1112 landing(&key.value).unwrap_or(GridPoint {
1113 x: last.right(),
1114 y: last.bottom(),
1115 })
1116 });
1117 vec![
1118 step(start, entry(region, seed)),
1119 step(reached, seed),
1120 step(end, far),
1121 ]
1122 }
1123 TrackKind::Morph => vec![step(start, center(first)), step(end, center(last))],
1124 TrackKind::Vanish | TrackKind::Emphasis | TrackKind::Overlay => hold(center(first)),
1125 TrackKind::Pantomime => hold(center(last)),
1128 }
1129}
1130
1131fn landing(value: &TrackValue) -> Option<GridPoint> {
1136 match value {
1137 TrackValue::Path(points) => points.last().copied(),
1138 _ => None,
1139 }
1140}
1141
1142fn step(at: Duration, to: GridPoint) -> Keyframe {
1143 Keyframe {
1144 at,
1145 value: TrackValue::Point(to),
1146 easing: Easing::EaseOut,
1147 }
1148}
1149
1150fn entry(region: GridRect, toward: GridPoint) -> GridPoint {
1154 let (left, right) = (region.left(), region.right());
1155 GridPoint {
1156 x: if toward.x - left <= right - toward.x {
1157 left
1158 } else {
1159 right
1160 },
1161 y: toward.y,
1162 }
1163}
1164
1165pub fn synthesize(before: &IndexedDocument<'_>, commit: &Commit) -> Timeline {
1168 let after = before.doc.try_apply(commit).ok();
1169 let scene = Scene {
1170 span: span(before, commit),
1171 paths: Paths::of(before.doc, after.as_ref(), commit),
1172 indexed: before,
1173 after,
1174 commit,
1175 };
1176 let cascade = Cascade::of(commit);
1177 let tracks: Vec<Track> = commit
1178 .ops()
1179 .iter()
1180 .map(|op| delayed(rule(&scene, op), cascade.start(&scene, op)))
1181 .collect();
1182 let duration = tracks
1183 .iter()
1184 .map(Track::end)
1185 .max()
1186 .unwrap_or(Duration::ZERO);
1187 let camera = camera_plan(&scene, &tracks);
1188 let pantomime = pantomime(&scene, commit.label(), &tracks);
1189 Timeline {
1190 label: commit.label().to_owned(),
1191 duration,
1192 camera,
1193 tracks,
1194 pantomime,
1195 }
1196}
1197
1198struct Scene<'a> {
1209 indexed: &'a IndexedDocument<'a>,
1210 after: Option<Document>,
1213 commit: &'a Commit,
1214 span: Duration,
1215 paths: Paths,
1216}
1217
1218struct Paths {
1229 before: HashMap<RouteId, Arc<[GridPoint]>>,
1230 after: HashMap<RouteId, Arc<[GridPoint]>>,
1231}
1232
1233impl Paths {
1234 fn of(before: &Document, after: Option<&Document>, commit: &Commit) -> Self {
1235 let mut was = Vec::new();
1238 let mut now = Vec::new();
1239 for op in commit.ops() {
1240 match op {
1241 OpCodes::Route(id, Crud::Create(_) | Crud::Restore) => now.push(*id),
1242 OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(_))) => {
1243 was.push(*id);
1244 now.push(*id);
1245 }
1246 OpCodes::Route(id, Crud::Delete | Crud::Update(_)) => was.push(*id),
1252 OpCodes::RouteLabel(id, _) => {
1253 if let Some(live) = before.route_label(id) {
1254 was.push(*live.as_ref().owner.as_ref());
1255 }
1256 }
1257 _ => {}
1258 }
1259 }
1260 Self {
1261 before: solved(before, &was),
1262 after: after.map(|doc| solved(doc, &now)).unwrap_or_default(),
1263 }
1264 }
1265}
1266
1267fn solved(doc: &Document, wanted: &[RouteId]) -> HashMap<RouteId, Arc<[GridPoint]>> {
1270 if wanted.is_empty() {
1271 return HashMap::default();
1272 }
1273 let mut index = DocIndex::default();
1274 let mut presentation = Presentation::default();
1275 presentation.refresh_routes(&index.view(doc));
1276 wanted
1277 .iter()
1278 .filter_map(|id| {
1279 let geometry = presentation.routes.get(id)?;
1280 let mut corners = vec![geometry.start_pos];
1281 corners.extend(geometry.iter_edges().map(|(_, edge)| edge.end));
1282 (corners.len() > 1).then(|| (*id, Arc::from(corners)))
1283 })
1284 .collect()
1285}
1286
1287fn slot_anchor(owner: Option<GridRect>, slot: PinSlot, body: GridRect) -> GridPoint {
1292 let Some(rect) = owner else {
1293 return center(body);
1294 };
1295 GridPoint {
1296 x: match slot.side {
1297 PinSide::West => rect.left(),
1298 PinSide::East => rect.right(),
1299 },
1300 y: pin_slot_row(rect.top(), slot.offset),
1301 }
1302}
1303
1304fn block_icon(doc: &Document, id: BlockId) -> Option<ScreenRect> {
1310 icon_box(doc.block(&id)?.as_ref().icon.as_ref())
1311}
1312
1313fn area_rect(doc: &Document, id: AreaId) -> Option<GridRect> {
1314 Some(*doc.area(&id)?.as_ref().rect.as_ref())
1315}
1316
1317fn text_pos(doc: &Document, id: TextId) -> Option<GridPoint> {
1318 Some(*doc.text(&id)?.as_ref().pos.as_ref())
1319}
1320
1321fn image_rect(doc: &Document, id: ImageId) -> Option<ScreenRect> {
1322 Some(*doc.image(&id)?.as_ref().rect.as_ref())
1323}
1324
1325fn pin_seat(doc: &Document, id: PinId) -> Option<(BlockId, PinSlot, GridRect)> {
1328 let pin = doc.pin(&id)?.as_ref();
1329 Some((*pin.owner.as_ref(), *pin.slot.as_ref(), *pin.rect.as_ref()))
1330}
1331
1332impl Scene<'_> {
1333 fn before(&self) -> &Document {
1335 self.indexed.doc
1336 }
1337
1338 fn after(&self) -> &Document {
1341 self.after.as_ref().unwrap_or(self.indexed.doc)
1342 }
1343
1344 fn minted(&self, id: BlockId) -> Option<&BlockInit> {
1348 self.commit.ops().iter().find_map(|op| match op {
1349 OpCodes::Block(other, Crud::Create(init)) if *other == id => Some(init),
1350 _ => None,
1351 })
1352 }
1353
1354 fn block_rect(&self, id: BlockId) -> Option<GridRect> {
1357 if let Some(live) = self.before().block(&id) {
1358 return Some(*live.as_ref().rect.as_ref());
1359 }
1360 self.minted(id).map(|init| init.rect)
1361 }
1362
1363 fn block_rect_after(&self, id: BlockId) -> Option<GridRect> {
1366 self.after()
1367 .block(&id)
1368 .map(|live| *live.as_ref().rect.as_ref())
1369 .or_else(|| self.block_rect(id))
1370 }
1371
1372 fn pin_site(&self, id: PinId) -> Option<Site> {
1376 let (owner, slot, body) = pin_seat(self.before(), id)?;
1377 Some(Site::Anchor(slot_anchor(
1378 self.block_rect(owner),
1379 slot,
1380 body,
1381 )))
1382 }
1383
1384 fn footprint(&self, side: Side, subject: EntityRef) -> TrackValue {
1390 let doc = side.doc(self);
1391 let value = match subject {
1392 EntityRef::Block(id) => side.block_rect(self, id).map(TrackValue::Rect),
1393 EntityRef::Pin(id) => pin_seat(doc, id).map(|(owner, slot, body)| {
1394 TrackValue::Point(slot_anchor(side.block_rect(self, owner), slot, body))
1395 }),
1396 EntityRef::Route(id) => side.paths(self).get(&id).cloned().map(TrackValue::Path),
1397 EntityRef::RouteLabel(id) => doc.route_label(&id).map(|live| TrackValue::Along {
1398 route: *live.as_ref().owner.as_ref(),
1399 at: *live.as_ref().pos.as_ref(),
1400 }),
1401 EntityRef::Text(id) => text_pos(doc, id).map(TrackValue::Point),
1402 EntityRef::Area(id) => area_rect(doc, id).map(TrackValue::Rect),
1403 EntityRef::Image(id) => image_rect(doc, id).map(TrackValue::Artwork),
1404 EntityRef::Document | EntityRef::Asset(_) => None,
1405 };
1406 value.unwrap_or(TrackValue::Settled)
1407 }
1408
1409 fn held(&self, subject: EntityRef) -> bool {
1413 let doc = self.before();
1414 match subject {
1415 EntityRef::Block(id) => doc.block(&id).is_some(),
1416 EntityRef::Pin(id) => doc.pin(&id).is_some(),
1417 EntityRef::Route(id) => doc.route(&id).is_some(),
1418 EntityRef::RouteLabel(id) => doc.route_label(&id).is_some(),
1419 EntityRef::Text(id) => doc.text(&id).is_some(),
1420 EntityRef::Area(id) => doc.area(&id).is_some(),
1421 EntityRef::Image(id) => doc.image(&id).is_some(),
1422 EntityRef::Document | EntityRef::Asset(_) => true,
1423 }
1424 }
1425
1426 fn container(&self, op: &OpCodes) -> Option<BlockId> {
1431 let doc = self.before();
1432 Some(match op {
1433 OpCodes::Block(_, Crud::Create(init)) => init.parent,
1434 OpCodes::Block(id, _) => *doc.block(id)?.as_ref().parent.as_ref(),
1435 OpCodes::Area(_, Crud::Create(init)) => init.owner,
1436 OpCodes::Area(id, _) => *doc.area(id)?.as_ref().owner.as_ref(),
1437 OpCodes::Text(_, Crud::Create(init)) => init.owner,
1438 OpCodes::Text(id, _) => *doc.text(id)?.as_ref().owner.as_ref(),
1439 OpCodes::Image(_, Crud::Create(init)) => init.owner,
1440 OpCodes::Image(id, _) => *doc.image(id)?.as_ref().owner.as_ref(),
1441 OpCodes::Route(_, Crud::Create(init)) => init.owner,
1442 OpCodes::Route(id, _) => *doc.route(id)?.as_ref().owner.as_ref(),
1443 OpCodes::RouteLabel(_, Crud::Create(init)) => {
1444 *doc.route(&init.owner)?.as_ref().owner.as_ref()
1445 }
1446 OpCodes::RouteLabel(id, _) => {
1447 let route = *doc.route_label(id)?.as_ref().owner.as_ref();
1448 *doc.route(&route)?.as_ref().owner.as_ref()
1449 }
1450 OpCodes::Pin(_, Crud::Create(init)) => init.owner,
1451 OpCodes::Pin(id, _) => *doc.pin(id)?.as_ref().owner.as_ref(),
1452 OpCodes::Document(_) | OpCodes::Asset(..) => return None,
1453 })
1454 }
1455
1456 fn scope_of(&self, op: &OpCodes) -> Option<Scope> {
1461 let inside = self.container(op)?;
1462 Some(Scope::from_wire(match op {
1463 OpCodes::Pin(_, Crud::Update(PinUpdate::Rect(_) | PinUpdate::FlipLR(_))) => inside,
1464 OpCodes::Pin(..) => self.parent_of(inside),
1465 _ => inside,
1466 }))
1467 }
1468
1469 fn parent_of(&self, block: BlockId) -> BlockId {
1471 self.before()
1472 .block(&block)
1473 .map(|live| *live.as_ref().parent.as_ref())
1474 .or_else(|| self.minted(block).map(|init| init.parent))
1475 .unwrap_or(BlockId::NULL)
1476 }
1477}
1478
1479#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1481enum Side {
1482 Before,
1483 After,
1484}
1485
1486impl Side {
1487 fn doc<'a>(self, scene: &'a Scene<'_>) -> &'a Document {
1488 match self {
1489 Side::Before => scene.before(),
1490 Side::After => scene.after(),
1491 }
1492 }
1493
1494 fn paths<'a>(self, scene: &'a Scene<'_>) -> &'a HashMap<RouteId, Arc<[GridPoint]>> {
1495 match self {
1496 Side::Before => &scene.paths.before,
1497 Side::After => &scene.paths.after,
1498 }
1499 }
1500
1501 fn block_rect(self, scene: &Scene<'_>, id: BlockId) -> Option<GridRect> {
1502 match self {
1503 Side::Before => scene.block_rect(id),
1504 Side::After => scene.block_rect_after(id),
1505 }
1506 }
1507}
1508
1509struct Cascade {
1516 doomed: HashSet<BlockId>,
1517}
1518
1519impl Cascade {
1520 fn of(commit: &Commit) -> Self {
1521 Self {
1522 doomed: commit
1523 .ops()
1524 .iter()
1525 .filter_map(|op| match op {
1526 OpCodes::Block(id, Crud::Delete) => Some(*id),
1527 _ => None,
1528 })
1529 .collect(),
1530 }
1531 }
1532
1533 fn start(&self, scene: &Scene<'_>, op: &OpCodes) -> Duration {
1534 if self.doomed.is_empty() {
1535 return Duration::ZERO;
1536 }
1537 let mut rank = 0;
1538 let mut inside = scene.container(op);
1539 while let Some(block) = inside {
1540 rank += u32::from(self.doomed.contains(&block));
1541 inside = (block != BlockId::NULL).then(|| scene.parent_of(block));
1542 }
1543 STAGGER * rank
1544 }
1545}
1546
1547fn span(before: &IndexedDocument<'_>, commit: &Commit) -> Duration {
1551 if commit.ops().iter().any(|op| arrives(before.doc, op)) {
1552 APPEAR
1553 } else {
1554 MORPH
1555 }
1556}
1557
1558fn arrives(before: &Document, op: &OpCodes) -> bool {
1563 fn creates<I, U>(crud: &Crud<I, U>) -> bool {
1564 matches!(crud, Crud::Create(_))
1565 }
1566 match op {
1567 OpCodes::Block(id, Crud::Update(BlockUpdate::Icon(icon))) => {
1568 icon_box(icon).is_some() && block_icon(before, *id).is_none()
1569 }
1570 OpCodes::Block(_, crud) => creates(crud),
1571 OpCodes::Pin(_, crud) => creates(crud),
1572 OpCodes::Route(_, crud) => creates(crud),
1573 OpCodes::RouteLabel(_, crud) => creates(crud),
1574 OpCodes::Text(_, crud) => creates(crud),
1575 OpCodes::Area(_, crud) => creates(crud),
1576 OpCodes::Image(_, crud) => creates(crud),
1577 OpCodes::Document(_) | OpCodes::Asset(..) => false,
1578 }
1579}
1580
1581fn icon_box(icon: &Icon) -> Option<ScreenRect> {
1584 (icon != &Icon::default()).then_some(icon.rect)
1585}
1586
1587fn rule(scene: &Scene<'_>, op: &OpCodes) -> Track {
1592 let span = scene.span;
1593 match op {
1594 OpCodes::Block(id, Crud::Create(init)) => {
1596 grow_in(EntityRef::Block(*id), init.rect, span, op)
1597 }
1598 OpCodes::Area(id, Crud::Create(init)) => grow_in(EntityRef::Area(*id), init.rect, span, op),
1599 OpCodes::Image(id, Crud::Create(init)) => {
1601 grow_in_artwork(EntityRef::Image(*id), init.rect, span, op)
1602 }
1603 OpCodes::Text(id, Crud::Create(init)) => {
1606 arrive(EntityRef::Text(*id), TrackValue::Point(init.pos), span, op)
1607 }
1608 OpCodes::Pin(id, Crud::Create(init)) => arrive(
1612 EntityRef::Pin(*id),
1613 TrackValue::Point(slot_anchor(
1614 scene.block_rect_after(init.owner),
1615 init.slot,
1616 init.rect,
1617 )),
1618 span,
1619 op,
1620 ),
1621 OpCodes::Route(id, Crud::Create(_)) => wire(EntityRef::Route(*id), scene, *id, op),
1623 OpCodes::RouteLabel(id, Crud::Create(init)) => arrive(
1627 EntityRef::RouteLabel(*id),
1628 TrackValue::Along {
1629 route: init.owner,
1630 at: init.pos,
1631 },
1632 span,
1633 op,
1634 ),
1635 OpCodes::Block(id, Crud::Update(BlockUpdate::Icon(icon))) => {
1639 match (block_icon(scene.before(), *id), icon_box(icon)) {
1640 (Some(from), None) => {
1643 vanish(EntityRef::Block(*id), TrackValue::Artwork(from), span, op)
1644 }
1645 (None, None) => settled(op),
1646 (None, Some(to)) => grow_in_artwork(EntityRef::Block(*id), to, span, op),
1647 (Some(from), Some(to)) => morph(
1648 EntityRef::Block(*id),
1649 TrackValue::Artwork(from),
1650 TrackValue::Artwork(to),
1651 span,
1652 op,
1653 ),
1654 }
1655 }
1656 OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(to))) => morph(
1660 EntityRef::Block(*id),
1661 TrackValue::Rect(scene.block_rect(*id).unwrap_or(*to)),
1662 TrackValue::Rect(*to),
1663 span,
1664 op,
1665 ),
1666 OpCodes::Area(id, Crud::Update(AreaUpdate::Rect(to))) => morph(
1669 EntityRef::Area(*id),
1670 TrackValue::Rect(area_rect(scene.before(), *id).unwrap_or(*to)),
1671 TrackValue::Rect(*to),
1672 span,
1673 op,
1674 ),
1675 OpCodes::Text(id, Crud::Update(TextUpdate::Pos(to))) => morph(
1676 EntityRef::Text(*id),
1677 TrackValue::Point(text_pos(scene.before(), *id).unwrap_or(*to)),
1678 TrackValue::Point(*to),
1679 span,
1680 op,
1681 ),
1682 OpCodes::Image(id, Crud::Update(ImageUpdate::Rect(to))) => morph(
1683 EntityRef::Image(*id),
1684 TrackValue::Artwork(image_rect(scene.before(), *id).unwrap_or(*to)),
1685 TrackValue::Artwork(*to),
1686 span,
1687 op,
1688 ),
1689 OpCodes::Pin(id, Crud::Update(PinUpdate::Rect(to))) => morph(
1692 EntityRef::Pin(*id),
1693 TrackValue::Rect(pin_seat(scene.before(), *id).map_or(*to, |(_, _, body)| body)),
1694 TrackValue::Rect(*to),
1695 span,
1696 op,
1697 ),
1698 OpCodes::Pin(id, Crud::Update(PinUpdate::Slot(to))) => reseated(scene, *id, *to, op),
1702 OpCodes::Pin(id, Crud::Update(PinUpdate::FlipLR(_))) => held(
1707 EntityRef::Pin(*id),
1708 pin_seat(scene.before(), *id)
1709 .map_or(TrackValue::Settled, |(_, _, body)| TrackValue::Rect(body)),
1710 span,
1711 op,
1712 ),
1713 OpCodes::Route(id, Crud::Update(RouteUpdate::Waypoints(_))) => rewired(scene, *id, op),
1717 OpCodes::RouteLabel(id, Crud::Update(RouteLabelUpdate::Pos(to))) => {
1723 slid(scene, *id, *to, op)
1724 }
1725 OpCodes::Block(id, Crud::Update(BlockUpdate::Title(update))) => {
1727 label_write(scene, LabelAt::BlockTitle(*id), update, op)
1728 }
1729 OpCodes::Block(id, Crud::Update(BlockUpdate::TypeLabel(update))) => {
1730 label_write(scene, LabelAt::BlockType(*id), update, op)
1731 }
1732 OpCodes::Area(id, Crud::Update(AreaUpdate::Title(update))) => {
1733 label_write(scene, LabelAt::AreaTitle(*id), update, op)
1734 }
1735 OpCodes::Pin(id, Crud::Update(PinUpdate::Name(to))) => {
1740 pin_line(scene, *id, PinLine::Name, to, op)
1741 }
1742 OpCodes::Pin(id, Crud::Update(PinUpdate::TypeName(to))) => {
1743 pin_line(scene, *id, PinLine::Type, to, op)
1744 }
1745 OpCodes::Pin(id, Crud::Update(PinUpdate::Tag(to))) => {
1746 pin_line(scene, *id, PinLine::Tag, to, op)
1747 }
1748 OpCodes::Pin(id, Crud::Update(PinUpdate::Dir(to))) => pin_flag(
1751 scene,
1752 *id,
1753 |pin| FlagState::Direction(*pin.dir.as_ref()),
1754 FlagState::Direction(*to),
1755 op,
1756 ),
1757 OpCodes::Pin(id, Crud::Update(PinUpdate::TagHidden(to))) => pin_flag(
1758 scene,
1759 *id,
1760 |pin| FlagState::Tag(TagVisibility::from(*pin.tag_hidden.as_ref())),
1761 FlagState::Tag(TagVisibility::from(*to)),
1762 op,
1763 ),
1764 OpCodes::Pin(id, Crud::Update(PinUpdate::PortAccent(to))) => pin_flag(
1765 scene,
1766 *id,
1767 |pin| FlagState::Accent(*pin.port_accent.as_ref()),
1768 FlagState::Accent(*to),
1769 op,
1770 ),
1771 OpCodes::Block(id, Crud::Update(BlockUpdate::Role(to))) => block_flag(
1774 scene,
1775 *id,
1776 |block| FlagState::Accent(*block.role.as_ref()),
1777 FlagState::Accent(*to),
1778 op,
1779 ),
1780 OpCodes::Block(id, Crud::Update(BlockUpdate::Locked(to))) => block_flag(
1781 scene,
1782 *id,
1783 |block| FlagState::Lock(InterfaceLock::from(*block.locked.as_ref())),
1784 FlagState::Lock(InterfaceLock::from(*to)),
1785 op,
1786 ),
1787 OpCodes::Area(id, Crud::Update(AreaUpdate::Role(to))) => or_settled(
1788 scene
1789 .before()
1790 .area(id)
1791 .filter(|area| area.is_alive())
1792 .map(|live| {
1793 let area = live.as_ref();
1794 stepped(
1795 Site::Shape(*area.rect.as_ref()),
1796 FlagState::Accent(*area.role.as_ref()),
1797 FlagState::Accent(*to),
1798 scene.span,
1799 op,
1800 )
1801 }),
1802 op,
1803 ),
1804 OpCodes::Text(id, Crud::Update(TextUpdate::Role(to))) => or_settled(
1805 scene
1806 .before()
1807 .text(id)
1808 .filter(|live| live.is_alive())
1809 .map(|live| {
1810 let box_ = live.as_ref();
1811 stepped(
1812 Site::Anchor(*box_.pos.as_ref()),
1813 FlagState::Accent(*box_.role.as_ref()),
1814 FlagState::Accent(*to),
1815 scene.span,
1816 op,
1817 )
1818 }),
1819 op,
1820 ),
1821 OpCodes::Route(id, Crud::Update(RouteUpdate::Role(to))) => or_settled(
1822 scene
1823 .before()
1824 .route(id)
1825 .filter(|route| route.is_alive())
1826 .map(|live| {
1827 stepped(
1828 Site::Wire,
1829 FlagState::Accent(*live.as_ref().role.as_ref()),
1830 FlagState::Accent(*to),
1831 scene.span,
1832 op,
1833 )
1834 }),
1835 op,
1836 ),
1837 OpCodes::Route(id, Crud::Update(RouteUpdate::Name(to))) => or_settled(
1841 scene
1842 .before()
1843 .route(id)
1844 .filter(|route| route.is_alive())
1845 .map(|live| {
1846 let of = Written {
1847 at: Site::Wire,
1848 line: TextLine::WireName,
1849 };
1850 crossfade(of, live.as_ref().name.as_ref(), to, scene.span, op)
1851 }),
1852 op,
1853 ),
1854 OpCodes::Text(id, Crud::Update(TextUpdate::Text(to))) => or_settled(
1856 scene
1857 .before()
1858 .text(id)
1859 .filter(|live| live.is_alive())
1860 .map(|live| {
1861 let box_ = live.as_ref();
1862 let of = Written {
1863 at: Site::Anchor(*box_.pos.as_ref()),
1864 line: TextLine::Content,
1865 };
1866 crossfade(of, box_.text.as_ref(), to, scene.span, op)
1867 }),
1868 op,
1869 ),
1870 OpCodes::Text(id, Crud::Delete) => emptied(scene, *id, op),
1874 OpCodes::Block(_, Crud::Delete)
1880 | OpCodes::Pin(_, Crud::Delete)
1881 | OpCodes::Route(_, Crud::Delete)
1882 | OpCodes::RouteLabel(_, Crud::Delete)
1883 | OpCodes::Area(_, Crud::Delete)
1884 | OpCodes::Image(_, Crud::Delete) => vanish(
1885 op.target(),
1886 scene.footprint(Side::Before, op.target()),
1887 span,
1888 op,
1889 ),
1890 OpCodes::Block(_, Crud::Restore)
1896 | OpCodes::Pin(_, Crud::Restore)
1897 | OpCodes::Route(_, Crud::Restore)
1898 | OpCodes::RouteLabel(_, Crud::Restore)
1899 | OpCodes::Text(_, Crud::Restore)
1900 | OpCodes::Area(_, Crud::Restore)
1901 | OpCodes::Image(_, Crud::Restore) => restored(scene, op),
1902 OpCodes::Block(_, Crud::Update(BlockUpdate::Parent(_)))
1907 | OpCodes::Pin(_, Crud::Update(PinUpdate::Owner(_)))
1908 | OpCodes::Route(_, Crud::Update(RouteUpdate::Owner(_)))
1909 | OpCodes::Text(_, Crud::Update(TextUpdate::Owner(_)))
1910 | OpCodes::Area(_, Crud::Update(AreaUpdate::Owner(_)))
1911 | OpCodes::Image(_, Crud::Update(ImageUpdate::Owner(_))) => held(
1912 op.target(),
1913 scene.footprint(Side::Before, op.target()),
1914 span,
1915 op,
1916 ),
1917 OpCodes::Document(TitleBlockUpdate::Top(id)) => held(
1918 EntityRef::Document,
1919 scene
1920 .block_rect(*id)
1921 .map_or(TrackValue::Settled, TrackValue::Rect),
1922 span,
1923 op,
1924 ),
1925 OpCodes::Document(TitleBlockUpdate::Name(to)) => crossfade(
1930 Written {
1931 at: Site::Sheet,
1932 line: TextLine::DocumentName,
1933 },
1934 scene.before().title_block().name.as_ref(),
1935 to,
1936 span,
1937 op,
1938 ),
1939 OpCodes::RouteLabel(id, Crud::Update(RouteLabelUpdate::Owner(to))) => held(
1944 EntityRef::RouteLabel(*id),
1945 scene
1946 .before()
1947 .route_label(id)
1948 .map_or(TrackValue::Settled, |live| TrackValue::Along {
1949 route: *to,
1950 at: *live.as_ref().pos.as_ref(),
1951 }),
1952 span,
1953 op,
1954 ),
1955 OpCodes::Image(id, Crud::Update(ImageUpdate::Asset(_))) => held(
1960 EntityRef::Image(*id),
1961 scene.footprint(Side::After, EntityRef::Image(*id)),
1962 span,
1963 op,
1964 ),
1965 OpCodes::Asset(..) => held(op.target(), TrackValue::Settled, span, op),
1971 }
1972}
1973
1974fn grow_in(subject: EntityRef, written: GridRect, span: Duration, op: &OpCodes) -> Track {
1978 let seed = GridRect {
1979 top_left: written.top_left,
1980 size: GridSize::default(),
1981 };
1982 appearing(
1983 subject,
1984 TrackValue::Rect(seed),
1985 TrackValue::Rect(written),
1986 span,
1987 op,
1988 )
1989}
1990
1991fn grow_in_artwork(subject: EntityRef, written: ScreenRect, span: Duration, op: &OpCodes) -> Track {
1993 let seed = ScreenRect {
1994 top_left: written.top_left,
1995 size: ScreenSize::default(),
1996 };
1997 appearing(
1998 subject,
1999 TrackValue::Artwork(seed),
2000 TrackValue::Artwork(written),
2001 span,
2002 op,
2003 )
2004}
2005
2006fn wire(subject: EntityRef, scene: &Scene<'_>, id: RouteId, op: &OpCodes) -> Track {
2009 let Some(path) = scene.paths.after.get(&id) else {
2010 return arrive(subject, TrackValue::Settled, scene.span, op);
2013 };
2014 appearing(
2015 subject,
2016 TrackValue::Path(Arc::from([path[0]])),
2017 TrackValue::Path(path.clone()),
2018 scene.span,
2019 op,
2020 )
2021}
2022
2023struct Ends {
2026 from: TrackValue,
2027 to: TrackValue,
2028}
2029
2030fn between(subject: EntityRef, kind: TrackKind, ends: Ends, span: Duration, op: &OpCodes) -> Track {
2032 Track {
2033 subject,
2034 kind,
2035 op: op.clone(),
2036 keys: vec![
2037 Keyframe {
2038 at: Duration::ZERO,
2039 value: ends.from,
2040 easing: Easing::Linear,
2041 },
2042 Keyframe {
2043 at: span,
2044 value: ends.to,
2045 easing: kind.arrival(),
2046 },
2047 ],
2048 }
2049}
2050
2051fn appearing(
2054 subject: EntityRef,
2055 seed: TrackValue,
2056 written: TrackValue,
2057 span: Duration,
2058 op: &OpCodes,
2059) -> Track {
2060 between(
2061 subject,
2062 TrackKind::Appear,
2063 Ends {
2064 from: seed,
2065 to: written,
2066 },
2067 span,
2068 op,
2069 )
2070}
2071
2072fn arrive(subject: EntityRef, value: TrackValue, span: Duration, op: &OpCodes) -> Track {
2076 Track {
2077 subject,
2078 kind: TrackKind::Appear,
2079 op: op.clone(),
2080 keys: vec![Keyframe {
2081 at: span,
2082 value,
2083 easing: Easing::EaseOut,
2084 }],
2085 }
2086}
2087
2088fn held(subject: EntityRef, value: TrackValue, span: Duration, op: &OpCodes) -> Track {
2091 Track {
2092 subject,
2093 kind: TrackKind::Emphasis,
2094 op: op.clone(),
2095 keys: vec![Keyframe {
2096 at: span,
2097 value,
2098 easing: Easing::Linear,
2099 }],
2100 }
2101}
2102
2103fn vanish(subject: EntityRef, footprint: TrackValue, span: Duration, op: &OpCodes) -> Track {
2109 let gone = match &footprint {
2110 TrackValue::Path(path) if !path.is_empty() => TrackValue::Path(Arc::from([path[0]])),
2111 standing => standing.clone(),
2112 };
2113 between(
2114 subject,
2115 TrackKind::Vanish,
2116 Ends {
2117 from: footprint,
2118 to: gone,
2119 },
2120 span,
2121 op,
2122 )
2123}
2124
2125fn restored(scene: &Scene<'_>, op: &OpCodes) -> Track {
2130 let subject = op.target();
2131 let to = scene.footprint(Side::After, subject);
2132 let from = match scene.footprint(Side::Before, subject) {
2133 TrackValue::Settled => to.clone(),
2134 stood => stood,
2135 };
2136 morph(subject, from, to, scene.span, op)
2137}
2138
2139fn delayed(mut track: Track, by: Duration) -> Track {
2142 if !by.is_zero() {
2143 for key in &mut track.keys {
2144 key.at += by;
2145 }
2146 }
2147 track
2148}
2149
2150fn morph(
2153 subject: EntityRef,
2154 from: TrackValue,
2155 to: TrackValue,
2156 span: Duration,
2157 op: &OpCodes,
2158) -> Track {
2159 between(subject, TrackKind::Morph, Ends { from, to }, span, op)
2160}
2161
2162fn crossfade(of: Written, was: &str, now: &str, span: Duration, op: &OpCodes) -> Track {
2165 morph(op.target(), text(of, was), text(of, now), span, op)
2166}
2167
2168fn text(of: Written, value: &str) -> TrackValue {
2169 TrackValue::Text {
2170 of,
2171 text: Arc::from(value),
2172 }
2173}
2174
2175fn stepped(at: Site, was: FlagState, now: FlagState, span: Duration, op: &OpCodes) -> Track {
2178 between(
2179 op.target(),
2180 TrackKind::Emphasis,
2181 Ends {
2182 from: TrackValue::Flag { at, state: was },
2183 to: TrackValue::Flag { at, state: now },
2184 },
2185 span,
2186 op,
2187 )
2188}
2189
2190fn pin_line(scene: &Scene<'_>, id: PinId, line: PinLine, now: &str, op: &OpCodes) -> Track {
2192 let (Some(at), Some(live)) = (scene.pin_site(id), scene.before().pin(&id)) else {
2193 return settled(op);
2194 };
2195 let of = Written {
2196 at,
2197 line: TextLine::Pin(line),
2198 };
2199 crossfade(of, line.of(live.as_ref()), now, scene.span, op)
2200}
2201
2202fn pin_flag(
2204 scene: &Scene<'_>,
2205 id: PinId,
2206 was: impl Fn(&Pin) -> FlagState,
2207 now: FlagState,
2208 op: &OpCodes,
2209) -> Track {
2210 let (Some(at), Some(live)) = (scene.pin_site(id), scene.before().pin(&id)) else {
2211 return settled(op);
2212 };
2213 stepped(at, was(live.as_ref()), now, scene.span, op)
2214}
2215
2216fn block_flag(
2218 scene: &Scene<'_>,
2219 id: BlockId,
2220 was: impl Fn(&Block) -> FlagState,
2221 now: FlagState,
2222 op: &OpCodes,
2223) -> Track {
2224 let (Some(rect), Some(live)) = (
2225 scene.block_rect(id),
2226 scene.before().block(&id).filter(|b| b.is_alive()),
2227 ) else {
2228 return settled(op);
2229 };
2230 stepped(Site::Shape(rect), was(live.as_ref()), now, scene.span, op)
2231}
2232
2233fn or_settled(track: Option<Track>, op: &OpCodes) -> Track {
2236 track.unwrap_or_else(|| settled(op))
2237}
2238
2239fn emptied(scene: &Scene<'_>, id: TextId, op: &OpCodes) -> Track {
2245 let Some(live) = scene.before().text(&id).filter(|t| t.is_alive()) else {
2246 return settled(op);
2247 };
2248 let of = Written {
2249 at: Site::Anchor(*live.as_ref().pos.as_ref()),
2250 line: TextLine::Content,
2251 };
2252 between(
2253 EntityRef::Text(id),
2254 TrackKind::Vanish,
2255 Ends {
2256 from: text(of, live.as_ref().text.as_ref()),
2257 to: text(of, ""),
2258 },
2259 scene.span,
2260 op,
2261 )
2262}
2263
2264fn reseated(scene: &Scene<'_>, id: PinId, to: PinSlot, op: &OpCodes) -> Track {
2270 let Some((owner, was, body)) = pin_seat(scene.before(), id) else {
2271 return settled(op);
2272 };
2273 let landed = pin_seat(scene.after(), id).map_or(body, |(_, _, body)| body);
2274 morph(
2275 EntityRef::Pin(id),
2276 TrackValue::Point(slot_anchor(scene.block_rect(owner), was, body)),
2277 TrackValue::Point(slot_anchor(scene.block_rect_after(owner), to, landed)),
2278 scene.span,
2279 op,
2280 )
2281}
2282
2283fn rewired(scene: &Scene<'_>, id: RouteId, op: &OpCodes) -> Track {
2289 let subject = EntityRef::Route(id);
2290 let (Some(was), Some(now)) = (scene.paths.before.get(&id), scene.paths.after.get(&id)) else {
2291 return held(subject, TrackValue::Settled, scene.span, op);
2292 };
2293 if was == now {
2294 return held(subject, TrackValue::Path(now.clone()), scene.span, op);
2295 }
2296 Track {
2297 subject,
2298 kind: TrackKind::Morph,
2299 op: op.clone(),
2300 keys: vec![
2301 Keyframe {
2302 at: Duration::ZERO,
2303 value: TrackValue::Path(was.clone()),
2304 easing: Easing::Linear,
2305 },
2306 Keyframe {
2307 at: scene.span / 2,
2308 value: TrackValue::Path(Arc::from([was[0]])),
2309 easing: Easing::Linear,
2310 },
2311 Keyframe {
2312 at: scene.span,
2313 value: TrackValue::Path(now.clone()),
2314 easing: Easing::EaseOut,
2315 },
2316 ],
2317 }
2318}
2319
2320fn slid(scene: &Scene<'_>, id: RouteLabelId, to: FracVal, op: &OpCodes) -> Track {
2322 let Some(live) = scene.before().route_label(&id).filter(|l| l.is_alive()) else {
2323 return settled(op);
2324 };
2325 let route = *live.as_ref().owner.as_ref();
2326 let from = *live.as_ref().pos.as_ref();
2327 morph(
2328 EntityRef::RouteLabel(id),
2329 TrackValue::Along { route, at: from },
2330 TrackValue::Along { route, at: to },
2331 scene.span,
2332 op,
2333 )
2334}
2335
2336#[derive(Clone, Copy)]
2338enum LabelAt {
2339 BlockTitle(BlockId),
2340 BlockType(BlockId),
2341 AreaTitle(AreaId),
2342}
2343
2344impl LabelAt {
2345 fn subject(self) -> EntityRef {
2346 match self {
2347 LabelAt::BlockTitle(id) | LabelAt::BlockType(id) => EntityRef::Block(id),
2348 LabelAt::AreaTitle(id) => EntityRef::Area(id),
2349 }
2350 }
2351
2352 fn slot(self) -> LabelSlot {
2353 match self {
2354 LabelAt::BlockTitle(_) | LabelAt::AreaTitle(_) => LabelSlot::Title,
2355 LabelAt::BlockType(_) => LabelSlot::TypeLabel,
2356 }
2357 }
2358
2359 fn label(self, doc: &Document) -> Option<&Label> {
2360 Some(match self {
2361 LabelAt::BlockTitle(id) => &doc.block(&id).filter(|b| b.is_alive())?.as_ref().title,
2362 LabelAt::BlockType(id) => &doc.block(&id).filter(|b| b.is_alive())?.as_ref().type_label,
2363 LabelAt::AreaTitle(id) => &doc.area(&id).filter(|a| a.is_alive())?.as_ref().title,
2364 })
2365 }
2366
2367 fn site(self, doc: &Document) -> Option<Site> {
2371 match self {
2372 LabelAt::BlockTitle(id) | LabelAt::BlockType(id) => doc
2373 .block(&id)
2374 .filter(|b| b.is_alive())
2375 .map(|live| *live.as_ref().rect.as_ref()),
2376 LabelAt::AreaTitle(id) => area_rect(doc, id),
2377 }
2378 .map(Site::Shape)
2379 }
2380}
2381
2382fn label_write(scene: &Scene<'_>, at: LabelAt, update: &LabelUpdate, op: &OpCodes) -> Track {
2390 let Some(label) = at.label(scene.before()) else {
2391 return settled(op);
2392 };
2393 let placed = |label: &Label| TrackValue::Label {
2394 slot: at.slot(),
2395 side: *label.side.as_ref(),
2396 offset: *label.offset.as_ref(),
2397 };
2398 let from = placed(label);
2399 let to = at.label(scene.after()).map_or_else(|| from.clone(), placed);
2400 match update {
2401 LabelUpdate::Offset(_) => morph(at.subject(), from, to, scene.span, op),
2402 LabelUpdate::Side(_) => held(at.subject(), to, scene.span, op),
2403 LabelUpdate::Name(now) => or_settled(
2404 at.site(scene.before()).map(|site| {
2405 let of = Written {
2406 at: site,
2407 line: TextLine::Label(at.slot()),
2408 };
2409 crossfade(of, label.name.as_ref(), now, scene.span, op)
2410 }),
2411 op,
2412 ),
2413 LabelUpdate::Hidden(now) => or_settled(
2419 at.site(scene.before()).map(|site| {
2420 stepped(
2421 site,
2422 FlagState::Label(at.slot(), LabelVisibility::from(*label.hidden.as_ref())),
2423 FlagState::Label(at.slot(), LabelVisibility::from(*now)),
2424 scene.span,
2425 op,
2426 )
2427 }),
2428 op,
2429 ),
2430 }
2431}
2432
2433fn settled(op: &OpCodes) -> Track {
2437 Track {
2438 subject: op.target(),
2439 kind: TrackKind::Emphasis,
2440 op: op.clone(),
2441 keys: vec![Keyframe {
2442 at: Duration::ZERO,
2443 value: TrackValue::Settled,
2444 easing: Easing::Linear,
2445 }],
2446 }
2447}
2448
2449fn spot(at: GridPoint) -> GridRect {
2452 GridRect {
2453 top_left: at,
2454 size: GridSize::default(),
2455 }
2456}
2457
2458fn center(r: GridRect) -> GridPoint {
2459 GridPoint {
2460 x: r.top_left.x + (r.size.w / 2) as i32,
2461 y: r.top_left.y + (r.size.h / 2) as i32,
2462 }
2463}
2464
2465fn grid_bounds(r: ScreenRect) -> GridRect {
2468 let (min, max) = px_corners(r);
2469 let cells = |v: f32, up: bool| {
2470 let scaled = v / GRID_SIZE;
2471 if up { scaled.ceil() } else { scaled.floor() }
2472 };
2473 let (left, top) = (cells(min[0], false), cells(min[1], false));
2474 let (right, bottom) = (cells(max[0], true), cells(max[1], true));
2475 GridRect {
2476 top_left: GridPoint {
2477 x: left as i32,
2478 y: top as i32,
2479 },
2480 size: GridSize {
2481 w: (right - left) as u32,
2482 h: (bottom - top) as u32,
2483 },
2484 }
2485}
2486
2487fn camera_plan(scene: &Scene<'_>, tracks: &[Track]) -> Option<CameraPlan> {
2497 let scope = tracks
2498 .iter()
2499 .find_map(|track| scene.scope_of(&track.op))
2500 .unwrap_or(Scope::Root);
2501 let region = tracks
2502 .iter()
2503 .filter(|track| scene.scope_of(&track.op).is_none_or(|owner| owner == scope))
2504 .filter_map(|track| track_region(scene, track))
2505 .reduce(union)?;
2506 Some(CameraPlan { scope, region })
2507}
2508
2509fn track_region(scene: &Scene<'_>, track: &Track) -> Option<GridRect> {
2514 track
2515 .keys
2516 .iter()
2517 .filter_map(|key| key.value.bounds())
2518 .reduce(union)
2519 .or_else(|| fallback_region(scene, track.subject))
2520}
2521
2522fn fallback_region(scene: &Scene<'_>, subject: EntityRef) -> Option<GridRect> {
2529 let resolve = |side| match scene.footprint(side, subject) {
2530 TrackValue::Along { route, .. } => scene.footprint(side, EntityRef::Route(route)).bounds(),
2531 value => value.bounds(),
2532 };
2533 resolve(Side::Before).or_else(|| resolve(Side::After))
2534}
2535
2536fn union(a: GridRect, b: GridRect) -> GridRect {
2537 let x0 = a.top_left.x.min(b.top_left.x);
2538 let y0 = a.top_left.y.min(b.top_left.y);
2539 let x1 = a.right().max(b.right());
2540 let y1 = a.bottom().max(b.bottom());
2541 GridRect {
2542 top_left: GridPoint { x: x0, y: y0 },
2543 size: GridSize {
2544 w: (x1 - x0) as u32,
2545 h: (y1 - y0) as u32,
2546 },
2547 }
2548}
2549
2550#[cfg(test)]
2551pub(crate) mod tests;