1use std::path::PathBuf;
2
3use egui::{Color32, Id, Key, Painter, Pos2, Rect, Sense, Stroke, Ui, Vec2};
4use glam::DVec3;
5use pfx_editor_style::Theme;
6use pfx_editor_style::widgets::Number;
7use pfx_editor_viewport::{Lens, Snap};
8use pfx_load::scene::tiles::{self, Tiles};
9
10use crate::editor::Editor;
11use crate::level::{Shape, Stroke as Brushed, Tool, stem};
12use crate::outline::Item;
13use crate::theme::{label, small};
14
15pub const STRIP_TOP: f32 = 34.0;
16const AXES: [&str; 3] = ["x", "y", "z"];
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Placing(pub PathBuf);
20
21fn outline_cell(painter: &Painter, lens: &Lens, layer: &Tiles, cell: [i32; 2], ink: Color32) {
22 let points: Vec<Pos2> = tiles::corners(layer, cell)
23 .iter()
24 .filter_map(|c| lens.project(DVec3::from_array(c.map(f64::from))))
25 .collect();
26 if points.len() < 4 {
27 return;
28 }
29 painter.add(egui::Shape::convex_polygon(
30 points,
31 ink.gamma_multiply(0.25),
32 Stroke::new(1.0_f32, ink),
33 ));
34}
35
36impl Editor {
37 pub fn level_keys(&mut self, ctx: &egui::Context, inside: bool) {
38 if !self.editable() {
39 return;
40 }
41 let pressed = |key: Key| ctx.input(|input| input.key_pressed(key));
42 let modifiers = ctx.input(|input| input.modifiers);
43 if modifiers.shift && pressed(Key::D) {
44 self.duplicate();
45 return;
46 }
47 if modifiers.shift {
48 return;
49 }
50 if pressed(Key::N) {
51 self.level.snapping.grid = !self.level.snapping.grid;
52 }
53 if pressed(Key::U) {
54 self.level.snapping.surface = !self.level.snapping.surface;
55 }
56 if pressed(Key::V) {
57 self.level.snapping.vertex = !self.level.snapping.vertex;
58 }
59 if pressed(Key::OpenBracket) {
60 let step = self.level.snapping.step / 2.0;
61 self.level.snapping.set_step(step);
62 }
63 if pressed(Key::CloseBracket) {
64 let step = self.level.snapping.step * 2.0;
65 self.level.snapping.set_step(step);
66 }
67 if pressed(Key::B) {
68 self.toggle_brush();
69 }
70 if self.level.brush.on {
71 if pressed(Key::E) {
72 self.level.brush.tool = match self.level.brush.tool {
73 Tool::Erase => Tool::Paint,
74 _ => Tool::Erase,
75 };
76 }
77 if pressed(Key::I) {
78 self.level.brush.tool = Tool::Pick;
79 }
80 }
81 if pressed(Key::P)
82 && inside
83 && let Some(stamp) = self.level.stamp.clone()
84 && let Some(at) = ctx.input(|input| input.pointer.hover_pos())
85 {
86 self.place(&stamp, at);
87 }
88 if pressed(Key::Escape) {
89 self.level.stamp = None;
90 self.level.brush.stroke = None;
91 }
92 }
93
94 pub fn snap_gizmo(&mut self) {
95 let snapping = self.level.snapping;
96 self.viewport.gizmo_mut().snap.distance = if snapping.grid {
97 snapping.step
98 } else {
99 Snap::default().distance
100 };
101 }
102
103 pub fn level_viewport(&mut self, ui: &mut Ui, image: Rect) {
104 let ctx = ui.ctx().clone();
105 let theme = Theme::of(&ctx);
106 if self.level.brush.on {
107 self.brush_input(ui, image);
108 }
109 let dropping = egui::DragAndDrop::payload::<Placing>(&ctx);
110 let (released, pointer) = ctx.input(|input| {
111 (
112 input.pointer.any_released(),
113 input.pointer.interact_pos().or(input.pointer.hover_pos()),
114 )
115 });
116 self.level.aim = None;
117 if let Some(placing) = &dropping
118 && let Some(at) = pointer.filter(|at| image.contains(*at))
119 {
120 if released {
121 egui::DragAndDrop::clear_payload(&ctx);
122 self.place(&placing.0, at);
123 } else {
124 self.level.aim = self.aim(&placing.0, at).ok();
125 }
126 }
127 let Some(lens) = self.lens() else {
128 return;
129 };
130 let painter = ui.painter_at(image);
131 if let Some(aim) = self.level.aim {
132 self.draw_aim(&painter, &lens, aim.point, aim.normal, &theme);
133 }
134 self.draw_picked(&painter, &lens, &theme);
135 if self.level.brush.on {
136 self.draw_brush(&painter, &lens, ui, image, &theme);
137 }
138 if self.level.shown() {
139 self.strip(&ctx, image);
140 }
141 }
142
143 fn draw_aim(&self, painter: &Painter, lens: &Lens, point: DVec3, normal: DVec3, theme: &Theme) {
144 let Some(centre) = lens.project(point) else {
145 return;
146 };
147 let reach = 0.25 * lens.metres_per_point(point) * 40.0;
148 if let Some(tip) = lens.project(point + normal * reach) {
149 painter.line_segment([centre, tip], Stroke::new(1.5_f32, theme.hot));
150 }
151 painter.circle_stroke(centre, 6.0, Stroke::new(1.5_f32, theme.hot));
152 }
153
154 fn draw_picked(&mut self, painter: &Painter, lens: &Lens, theme: &Theme) {
155 if self.level.picked.len() < 2 {
156 return;
157 }
158 let primary = self.selected_object().map(str::to_string);
159 let picked = self.level.picked.clone();
160 let indices: Vec<usize> = {
161 let Some(scene) = self.viewport.scene() else {
162 return;
163 };
164 picked
165 .iter()
166 .filter(|name| Some(*name) != primary.as_ref())
167 .filter_map(|name| scene.objects.iter().position(|o| &o.name == name))
168 .collect()
169 };
170 let Some(surfaces) = self.surfaces() else {
171 return;
172 };
173 for index in indices {
174 pfx_editor_viewport::overlay::outline(
175 painter,
176 lens,
177 &surfaces.boxes(index),
178 theme.accent,
179 1.0,
180 );
181 }
182 }
183
184 fn brush_input(&mut self, ui: &mut Ui, image: Rect) {
185 let response = ui.interact(image, Id::new("pfx edit brush"), Sense::click_and_drag());
186 let (line, horizontal, vertical) = ui.ctx().options(|options| {
187 (
188 options.input_options.line_scroll_speed,
189 options.input_options.horizontal_scroll_modifier,
190 options.input_options.vertical_scroll_modifier,
191 )
192 });
193 let (origin, pointer, middle, shift, ctrl, scroll) = ui.input(|input| {
194 (
195 input.pointer.press_origin(),
196 input.pointer.interact_pos(),
197 input.pointer.middle_down(),
198 input.modifiers.shift,
199 input.modifiers.ctrl,
200 pfx_editor_viewport::scrolled(input, line, horizontal, vertical),
201 )
202 });
203 if response.hovered()
204 && scroll != 0.0
205 && let Some(eye) = self.viewport.eye()
206 {
207 self.viewport.set_eye(eye.dolly(f64::from(scroll)));
208 }
209 if response.drag_started()
210 && let Some(origin) = origin
211 {
212 self.level.brush.grab = None;
213 self.level.brush.stroke = None;
214 if middle {
215 self.level.brush.grab = self.viewport.eye().map(|eye| (eye, origin, shift));
216 } else if let Some(cell) = self.cell_under(origin) {
217 let shape = if shift {
218 Shape::Line
219 } else if ctrl {
220 Shape::Rectangle
221 } else {
222 Shape::Free
223 };
224 self.level.brush.stroke = Some(Brushed::new(shape, cell));
225 }
226 }
227 if response.dragged()
228 && let Some(at) = pointer
229 {
230 match self.level.brush.grab {
231 Some((eye, from, pan)) => {
232 let moved = [f64::from(at.x - from.x), f64::from(at.y - from.y)];
233 self.viewport.set_eye(if pan {
234 eye.pan(moved, f64::from(image.height()))
235 } else {
236 eye.turn(moved)
237 });
238 }
239 None => {
240 if let Some(cell) = self.cell_under(at)
241 && let Some(stroke) = &mut self.level.brush.stroke
242 {
243 stroke.reach(cell);
244 }
245 }
246 }
247 }
248 if response.drag_stopped() {
249 self.level.brush.grab = None;
250 self.finish_stroke();
251 }
252 if response.clicked()
253 && let Some(at) = pointer
254 && let Some(cell) = self.cell_under(at)
255 {
256 self.level.brush.stroke = Some(Brushed::new(Shape::Free, cell));
257 self.finish_stroke();
258 }
259 }
260
261 fn draw_brush(&mut self, painter: &Painter, lens: &Lens, ui: &Ui, image: Rect, theme: &Theme) {
262 let Some(layer) = self.brush_layer() else {
263 return;
264 };
265 let ink = match self.level.brush.tool {
266 Tool::Paint => theme.hot,
267 Tool::Erase => theme.error,
268 Tool::Pick => theme.secondary,
269 };
270 if let Some(stroke) = &self.level.brush.stroke {
271 for cell in stroke.cells() {
272 outline_cell(painter, lens, &layer, cell, ink);
273 }
274 } else if let Some(at) = ui.input(|input| input.pointer.hover_pos())
275 && image.contains(at)
276 && let Some(cell) = self.cell_under(at)
277 {
278 outline_cell(painter, lens, &layer, cell, ink);
279 }
280 }
281
282 fn strip(&mut self, ctx: &egui::Context, image: Rect) {
283 let theme = Theme::of(ctx);
284 egui::Area::new(Id::new("pfx edit level strip"))
285 .order(egui::Order::Foreground)
286 .fixed_pos(image.left_top() + Vec2::new(8.0, STRIP_TOP))
287 .show(ctx, |ui| {
288 egui::Frame::NONE
289 .fill(theme.surface)
290 .stroke(Stroke::new(theme.stroke, theme.line))
291 .inner_margin(egui::Margin::symmetric(6, 3))
292 .show(ui, |ui| {
293 ui.horizontal(|ui| self.strip_row(ui));
294 });
295 });
296 }
297
298 fn strip_row(&mut self, ui: &mut Ui) {
299 let theme = Theme::of(ui.ctx());
300 let snapping = self.level.snapping;
301 if self.button(ui, "grid", "N", "grid", snapping.grid) {
302 self.level.snapping.grid = !snapping.grid;
303 }
304 let mut step = self.level.snapping.step as f32;
305 let response = ui.add(Number::new(&mut step).speed(0.01).decimals(2).width(44.0));
306 self.layout.rows.insert("step".to_string(), response.rect);
307 if response.changed() {
308 self.level.snapping.set_step(f64::from(step));
309 }
310 if self.button(ui, "surface", "U", "surface", snapping.surface) {
311 self.level.snapping.surface = !snapping.surface;
312 }
313 if self.button(ui, "align", "·", "normal", snapping.align) {
314 self.level.snapping.align = !snapping.align;
315 }
316 if self.button(ui, "vertex", "V", "vertex", snapping.vertex) {
317 self.level.snapping.vertex = !snapping.vertex;
318 }
319 if let Some(stamp) = &self.level.stamp {
320 ui.separator();
321 ui.label(small(
322 &theme,
323 format!("P places {}", stem(stamp)),
324 theme.text2,
325 ));
326 }
327 ui.separator();
328 let on = self.level.brush.on;
329 if self.button(ui, "brush", "B", "tiles", on) {
330 self.toggle_brush();
331 }
332 }
333
334 pub fn level_panel(&mut self, ui: &mut Ui) -> bool {
335 let tile = self.selected_object().map(str::to_string).filter(|name| {
336 self.level.picked.len() < 2 && pfx_load::scene::tiles::cell_of(name).is_some()
337 });
338 if let Some(name) = tile
339 && self.is_cell(&name)
340 {
341 let theme = Theme::of(ui.ctx());
342 ui.label(small(
343 &theme,
344 format!("a tile of its layer; B turns the brush on to paint or erase it ({name})"),
345 theme.muted,
346 ));
347 return true;
348 }
349 if self.level.brush.on || matches!(self.selection, Some(Item::Layer(_))) {
350 self.brush_panel(ui);
351 return true;
352 }
353 if self.level.picked.len() > 1 {
354 self.arrange_panel(ui);
355 return true;
356 }
357 false
358 }
359
360 fn brush_panel(&mut self, ui: &mut Ui) {
361 let theme = Theme::of(ui.ctx());
362 let note = |text: &str, colour: Color32| small(&theme, text, colour);
363 let Some(layer) = self.brush_layer() else {
364 ui.label(note(
365 "no tile layer: name a tiles file in the scene's [authoring.pfx] tiles",
366 theme.muted,
367 ));
368 return;
369 };
370 let names: Vec<String> = self.layers().into_iter().map(|l| l.name).collect();
371 ui.label(label(&theme, "layer", theme.text2));
372 let mut chosen = layer.name.clone();
373 egui::ComboBox::from_id_salt("brush layer")
374 .selected_text(note(&chosen, theme.text))
375 .show_ui(ui, |ui| {
376 for name in &names {
377 ui.selectable_value(&mut chosen, name.clone(), name.as_str());
378 }
379 });
380 if chosen != layer.name {
381 self.level.brush.layer = Some(chosen);
382 }
383 ui.label(note(
384 &format!(
385 "{} plane · cell {} × {} · {} cells",
386 ["xy", "xz", "yz"][layer.plane.unwrap_or_default() as usize],
387 layer.cell_sides()[0],
388 layer.cell_sides()[1],
389 layer.cells().len()
390 ),
391 theme.muted,
392 ));
393 ui.add_space(4.0);
394 ui.horizontal(|ui| {
395 for (name, key, tool) in [
396 ("paint", "B", Tool::Paint),
397 ("erase", "E", Tool::Erase),
398 ("pick", "I", Tool::Pick),
399 ] {
400 let lit = self.level.brush.on && self.level.brush.tool == tool;
401 if self.button(ui, name, key, tool.word(), lit) {
402 self.level.brush.on = true;
403 self.level.brush.tool = tool;
404 }
405 }
406 });
407 ui.add_space(4.0);
408 ui.label(label(&theme, "prefab", theme.text2));
409 let current = self.brush_prefab();
410 for (key, prefab) in &layer.palette {
411 let lit = current.as_deref() == Some(prefab.as_str());
412 let response = ui.selectable_label(lit, note(&format!("{key} {prefab}"), theme.code));
413 self.layout.buttons.insert("palette", response.rect);
414 if response.clicked() {
415 self.level.brush.prefab = Some(prefab.clone());
416 }
417 }
418 if let Some(prefab) = ¤t
419 && tiles::key(&layer, prefab).is_none()
420 {
421 ui.label(note(
422 &format!("+ {prefab} (joins the palette)"),
423 theme.hot_dim,
424 ));
425 }
426 ui.add_space(4.0);
427 ui.label(note(
428 "drag paints · shift-drag a line · ctrl-drag a rectangle · middle-drag turns",
429 theme.muted,
430 ));
431 }
432
433 fn arrange_panel(&mut self, ui: &mut Ui) {
434 let theme = Theme::of(ui.ctx());
435 let picked = self.level.picked.clone();
436 ui.label(small(
437 &theme,
438 format!("{} objects: {}", picked.len(), picked.join(", ")),
439 theme.text2,
440 ));
441 ui.add_space(4.0);
442 ui.label(label(&theme, "duplicate", theme.text2));
443 ui.horizontal(|ui| {
444 let mut offset = self.level.offset;
445 let mut changed = false;
446 for value in offset.iter_mut() {
447 changed |= ui
448 .add(Number::new(value).speed(0.05).decimals(2).width(52.0))
449 .changed();
450 }
451 if changed {
452 self.level.offset = offset;
453 }
454 if self.button(ui, "duplicate", "⇧D", "duplicate", false) {
455 self.duplicate();
456 }
457 });
458 ui.add_space(4.0);
459 for (word, last) in [("align to first", false), ("align to last", true)] {
460 ui.horizontal(|ui| {
461 ui.label(label(&theme, word, theme.text2));
462 for (axis, name) in AXES.iter().enumerate() {
463 let id: &'static str = match (last, axis) {
464 (false, 0) => "align first x",
465 (false, 1) => "align first y",
466 (false, _) => "align first z",
467 (true, 0) => "align last x",
468 (true, 1) => "align last y",
469 (true, _) => "align last z",
470 };
471 if self.button(ui, id, name, "", false) {
472 self.align(axis, last);
473 }
474 }
475 });
476 }
477 ui.horizontal(|ui| {
478 ui.label(label(&theme, "distribute", theme.text2));
479 for (axis, name) in AXES.iter().enumerate() {
480 let id: &'static str = ["distribute x", "distribute y", "distribute z"][axis];
481 if self.button(ui, id, name, "", false) {
482 self.distribute(axis);
483 }
484 }
485 });
486 }
487}