Skip to main content

pfx_editor/
ui_level.rs

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) = &current
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}