1use fission_ir::{CanvasSelectionPolicy, CanvasTarget, CanvasTargetKind, WidgetId};
2use fission_layout::{LayoutPoint, LayoutRect, LayoutSnapshot};
3use serde::{Deserialize, Serialize};
4
5use crate::event::PointerKind;
6use crate::input::viewport::ViewportStateMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum CanvasInteractionPhase {
10 Start,
11 Update,
12 End,
13 Activate,
14 Cancel,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum CanvasInteractionKind {
19 SelectNode { node_id: u128 },
20 MoveNode { node_id: u128 },
21 ResizeNode { node_id: u128, handle: u8 },
22 SelectEdge { edge_id: u128 },
23 Marquee,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct CanvasInteraction {
28 pub canvas_id: WidgetId,
29 pub target_id: WidgetId,
30 pub kind: CanvasInteractionKind,
31 pub selection_policy: CanvasSelectionPolicy,
32 pub phase: CanvasInteractionPhase,
33 pub input_kind: PointerKind,
34 pub modifiers: u8,
36 pub screen_point: LayoutPoint,
37 pub world_point: LayoutPoint,
38 pub screen_delta: LayoutPoint,
39 pub world_delta: LayoutPoint,
40 pub bounds_before: Option<LayoutRect>,
41 pub bounds_after: Option<LayoutRect>,
42 pub marquee: Option<LayoutRect>,
43}
44
45pub(crate) fn canvas_interaction(
46 target_id: WidgetId,
47 target: &CanvasTarget,
48 phase: CanvasInteractionPhase,
49 point: LayoutPoint,
50 delta: LayoutPoint,
51 drag_start: Option<LayoutPoint>,
52 layout: &LayoutSnapshot,
53 viewports: &ViewportStateMap,
54 input_kind: PointerKind,
55 modifiers: u8,
56) -> CanvasInteraction {
57 let canvas_id = WidgetId::from_u128(target.canvas_id);
58 let viewer_rect = layout
59 .get_node_rect(canvas_id)
60 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
61 let transform = viewports.transform(canvas_id).unwrap_or_default();
62 let local = [point.x - viewer_rect.x(), point.y - viewer_rect.y()];
63 let world = transform.screen_to_world(local);
64 let world_point = LayoutPoint::new(world[0], world[1]);
65 let world_delta = LayoutPoint::new(delta.x / transform.scale, delta.y / transform.scale);
66 let (kind, bounds_before, bounds_after, marquee) = match &target.kind {
67 CanvasTargetKind::Node { node_id, bounds } => {
68 let before = rect(*bounds);
69 let mut after = before;
70 if matches!(phase, CanvasInteractionPhase::Update) {
71 after.origin.x += world_delta.x;
72 after.origin.y += world_delta.y;
73 after.origin.x = snap(after.origin.x, target);
74 after.origin.y = snap(after.origin.y, target);
75 }
76 (
77 if matches!(phase, CanvasInteractionPhase::Activate) {
78 CanvasInteractionKind::SelectNode { node_id: *node_id }
79 } else {
80 CanvasInteractionKind::MoveNode { node_id: *node_id }
81 },
82 Some(before),
83 Some(after),
84 None,
85 )
86 }
87 CanvasTargetKind::ResizeHandle {
88 node_id,
89 handle,
90 bounds,
91 } => {
92 let before = rect(*bounds);
93 let after = resize(before, *handle, world_delta, target);
94 (
95 CanvasInteractionKind::ResizeNode {
96 node_id: *node_id,
97 handle: *handle,
98 },
99 Some(before),
100 Some(after),
101 None,
102 )
103 }
104 CanvasTargetKind::Edge { edge_id, .. } => (
105 CanvasInteractionKind::SelectEdge { edge_id: *edge_id },
106 None,
107 None,
108 None,
109 ),
110 CanvasTargetKind::Marquee => {
111 let marquee = drag_start.map(|start| {
112 let start_local = [start.x - viewer_rect.x(), start.y - viewer_rect.y()];
113 let start_world = transform.screen_to_world(start_local);
114 normalized_rect(
115 LayoutPoint::new(start_world[0], start_world[1]),
116 world_point,
117 )
118 });
119 (CanvasInteractionKind::Marquee, None, None, marquee)
120 }
121 };
122 CanvasInteraction {
123 canvas_id,
124 target_id,
125 kind,
126 selection_policy: target.selection_policy,
127 phase,
128 input_kind,
129 modifiers,
130 screen_point: point,
131 world_point,
132 screen_delta: delta,
133 world_delta,
134 bounds_before,
135 bounds_after,
136 marquee,
137 }
138}
139
140fn rect(bounds: [f32; 4]) -> LayoutRect {
141 LayoutRect::new(bounds[0], bounds[1], bounds[2], bounds[3])
142}
143
144fn snap(value: f32, target: &CanvasTarget) -> f32 {
145 let Some(spacing) = target.snap_spacing.filter(|spacing| *spacing > 0.0) else {
146 return value;
147 };
148 let candidate = (value / spacing).round() * spacing;
149 if target.snap_threshold <= 0.0 || (candidate - value).abs() <= target.snap_threshold {
150 candidate
151 } else {
152 value
153 }
154}
155
156fn resize(before: LayoutRect, handle: u8, delta: LayoutPoint, target: &CanvasTarget) -> LayoutRect {
157 let mut left = before.x();
158 let mut top = before.y();
159 let mut right = before.right();
160 let mut bottom = before.bottom();
161 if matches!(handle, 0 | 6 | 7) {
162 left = snap(left + delta.x, target).min(right - 1.0);
163 }
164 if matches!(handle, 2 | 3 | 4) {
165 right = snap(right + delta.x, target).max(left + 1.0);
166 }
167 if matches!(handle, 0 | 1 | 2) {
168 top = snap(top + delta.y, target).min(bottom - 1.0);
169 }
170 if matches!(handle, 4 | 5 | 6) {
171 bottom = snap(bottom + delta.y, target).max(top + 1.0);
172 }
173 LayoutRect::new(left, top, right - left, bottom - top)
174}
175
176fn normalized_rect(first: LayoutPoint, second: LayoutPoint) -> LayoutRect {
177 let left = first.x.min(second.x);
178 let top = first.y.min(second.y);
179 LayoutRect::new(
180 left,
181 top,
182 (first.x - second.x).abs(),
183 (first.y - second.y).abs(),
184 )
185}