1use blockworx_doc::block_model::Asset;
2use blockworx_geom::{Pos2, Rect, WorldPx, pos2, vec2};
3
4use blockworx_paint::{AnimKey, Animator, Renderer};
5
6use std::time::Duration;
7
8use crate::{
9 grid::{GRID_SIZE, LOCK_HINT_SIZE, round_to_grid},
10 state::ResizeMode,
11 theme::{Role, Style},
12};
13
14const LOCK_HINT_SVG: &str = include_str!("../../../../icons/icon-lock.svg");
17
18pub const NEW_PIN_INACTIVE_SCALE: f32 = 0.49;
21
22pub const NEW_PIN_GROW_RANGE: WorldPx = WorldPx::new(GRID_SIZE);
26
27pub const NEW_PIN_ANIM_TIME: Duration = Duration::from_millis(120);
29
30pub const HANDLE_RADIUS: WorldPx = WorldPx::new(GRID_SIZE * 0.45);
32
33pub const HANDLE_GRAB: WorldPx = WorldPx::new(2.0 * HANDLE_RADIUS.get());
36
37pub const RESIZE_INACTIVE_SCALE: f32 = NEW_PIN_INACTIVE_SCALE * 1.3;
41
42pub fn resize_handles(bbox: Rect) -> [(ResizeMode, Pos2); 4] {
45 [
46 (ResizeMode::LeftTop, bbox.left_top()),
47 (ResizeMode::RightTop, bbox.right_top()),
48 (ResizeMode::LeftBottom, bbox.left_bottom()),
49 (ResizeMode::RightBottom, bbox.right_bottom()),
50 ]
51}
52
53pub fn resize_handle_at(bbox: Rect, pos: Pos2) -> Option<ResizeMode> {
55 resize_handles(bbox)
56 .into_iter()
57 .find_map(|(mode, corner)| HANDLE_GRAB.within(corner, pos).then_some(mode))
58}
59
60fn anim_key(mode: ResizeMode, corner: Pos2) -> AnimKey {
65 AnimKey::of((
66 "resize_corner",
67 mode,
68 round_to_grid(corner.x) as i32,
69 round_to_grid(corner.y) as i32,
70 ))
71}
72
73fn draw_armed_handle(pos: Pos2, painter: &mut Style<'_, impl Renderer>) {
76 painter.circle(
77 pos,
78 HANDLE_RADIUS,
79 Role::ResizeCornerActiveFill,
80 (1.0, Role::ResizeCornerActiveStroke),
81 );
82}
83
84fn grown(mode: ResizeMode, corner: Pos2, pointer: Option<Pos2>, anim: &dyn Animator) -> f32 {
88 let goal = if pointer.is_some_and(|p| NEW_PIN_GROW_RANGE.within(corner, p)) {
89 1.0
90 } else {
91 0.0
92 };
93 anim.animate(anim_key(mode, corner), goal, NEW_PIN_ANIM_TIME)
94}
95
96fn draw_corner_handle(pos: Pos2, t: f32, painter: &mut Style<'_, impl Renderer>) {
100 if t < 1.0 {
101 painter.with_opacity(1.0 - t, |p| {
102 p.circle_filled(
103 pos,
104 HANDLE_RADIUS * RESIZE_INACTIVE_SCALE,
105 Role::ResizeCornerFill,
106 );
107 });
108 }
109 if t > 0.0 {
110 let radius = HANDLE_RADIUS * (RESIZE_INACTIVE_SCALE + (1.0 - RESIZE_INACTIVE_SCALE) * t);
111 painter.with_opacity(t, |p| {
112 p.circle(
113 pos,
114 radius,
115 Role::ResizeCornerFill,
116 (0.5, Role::ResizeCornerStroke),
117 );
118 });
119 }
120}
121
122pub fn draw_selection_frame(
132 bbox: Rect,
133 mode: Option<ResizeMode>,
134 painter: &mut Style<'_, impl Renderer>,
135) {
136 let handles = resize_handles(bbox);
137 let growths = painter.animator().map(|anim| {
138 let pointer = anim.pointer_world();
139 handles.map(|(corner, at)| (mode != Some(corner)).then(|| grown(corner, at, pointer, anim)))
142 });
143 let mut armed = None;
144 for ((corner, at), growth) in handles.into_iter().zip(growths.unwrap_or([None; 4])) {
145 if mode == Some(corner) {
146 armed = Some(at);
147 continue;
148 }
149 match growth {
150 Some(t) => draw_corner_handle(at, t, painter),
151 None => painter.circle(
152 at,
153 HANDLE_RADIUS,
154 Role::ResizeCornerFill,
155 (0.5, Role::ResizeCornerStroke),
156 ),
157 }
158 }
159 if let Some(at) = armed {
160 draw_armed_handle(at, painter);
161 }
162}
163
164pub fn draw_lock_hint(bbox: Rect, painter: &mut Style<'_, impl Renderer>) {
170 let s = LOCK_HINT_SIZE;
171 let m = HANDLE_RADIUS.get() + GRID_SIZE * 0.2;
172 if bbox.width() < s + m || bbox.height() < s + m {
173 return;
174 }
175
176 let icon = Rect::from_min_size(
177 pos2(bbox.right_top().x - m - s, bbox.right_top().y + m),
178 vec2(s, s),
179 );
180 let color = painter.theme().resolve(Role::LockedHintIcon);
181 let tinted = LOCK_HINT_SVG.replace(
182 "stroke=\"#ffffff\"",
183 &format!(
184 "stroke=\"#{:02x}{:02x}{:02x}\" stroke-opacity=\"{:.3}\"",
185 color.r(),
186 color.g(),
187 color.b(),
188 color.a() as f32 / 255.0
189 ),
190 );
191 painter.draw_image(icon, &Asset::Svg(tinted.into_bytes().into()));
192}