mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 18:18:17 -06:00
3D: test the New 3D Primitive dialog from a blank comp (#1734)
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//! Layer ▸ New ▸ 3D Primitive… (#1734): the dialog picks a type, name and dimensions and runs
|
||||
//! `layer.newPrimitive`, which switches a Classic 3D comp to Advanced 3D in the same step.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::{Layer, LayerSource, PrimitiveKind, Renderer};
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
|
||||
use egui::{Event, Key, Modifiers, PointerButton};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, EffectcraftApp> {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Scene", "width": 640, "height": 360, "duration": 2})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.step();
|
||||
h
|
||||
}
|
||||
|
||||
/// An automation element's rect in the last frame drawn (a dialog settles its place in its
|
||||
/// first frames).
|
||||
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Option<[f32; 4]> {
|
||||
let auto = &h.state().auto;
|
||||
auto.elements.iter().chain(auto.previous.iter()).find(|e| e.id == id).map(|e| e.rect)
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
||||
let [x, y, w, hh] = rect(h, id).unwrap_or_else(|| panic!("{id} is registered"));
|
||||
let p = egui::pos2(x + w / 2.0, y + hh / 2.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) {
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed, repeat: false, modifiers });
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Types `text` over a field's contents and leaves it with Tab.
|
||||
fn type_into(h: &mut Harness<'_, EffectcraftApp>, id: &str, text: &str) {
|
||||
click(h, id);
|
||||
assert!(h.ctx.egui_wants_keyboard_input(), "{id} has focus");
|
||||
key(h, Key::A, Modifiers::COMMAND);
|
||||
h.input_mut().events.push(Event::Text(text.into()));
|
||||
h.step();
|
||||
key(h, Key::Tab, Modifiers::NONE);
|
||||
}
|
||||
|
||||
/// Layer ▸ New ▸ 3D Primitive…, as the menu runs it.
|
||||
fn open(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
let (path, entry) =
|
||||
effectcraft_engine::menus::entries().into_iter().find(|(_, e)| e.label == "3D Primitive...").expect("Layer ▸ New lists 3D Primitive...");
|
||||
assert_eq!(path, ["Layer", "New"]);
|
||||
assert_eq!(entry.command, "layer.newPrimitive");
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, &entry.command, entry.params_or_empty()).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::NewPrimitive));
|
||||
}
|
||||
|
||||
fn choose(h: &mut Harness<'_, EffectcraftApp>, kind: PrimitiveKind) {
|
||||
click(h, "dialog.primitive.type");
|
||||
let i = PrimitiveKind::ALL.iter().position(|k| *k == kind).unwrap();
|
||||
click(h, &format!("dialog.primitive.typeItem.{i}"));
|
||||
}
|
||||
|
||||
fn layers(h: &Harness<'_, EffectcraftApp>) -> Vec<Layer> {
|
||||
h.state().session.active_comp().unwrap().layers.clone()
|
||||
}
|
||||
|
||||
fn renderer(h: &Harness<'_, EffectcraftApp>) -> Renderer {
|
||||
h.state().session.active_comp().unwrap().renderer
|
||||
}
|
||||
|
||||
fn geometry(l: &Layer, id: &str) -> f64 {
|
||||
l.props.prop(&format!("geometryOptions/{id}")).unwrap_or_else(|| panic!("{id}")).value.as_f64()
|
||||
}
|
||||
|
||||
/// A torus named "Ring" with a 250 px radius, the rest left at their defaults.
|
||||
fn create_ring(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
open(h);
|
||||
choose(h, PrimitiveKind::Torus);
|
||||
type_into(h, "dialog.primitive.name", "Ring");
|
||||
type_into(h, "dialog.primitive.radius", "250");
|
||||
click(h, "dialog.primitive.ok");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
}
|
||||
|
||||
fn assert_ring(l: &Layer) {
|
||||
assert_eq!(l.source, LayerSource::Primitive { kind: PrimitiveKind::Torus });
|
||||
assert_eq!(l.name, "Ring");
|
||||
assert_eq!(geometry(l, "radius"), 250.0);
|
||||
assert_eq!(geometry(l, "tubeRadius"), 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_a_primitive_in_a_classic_3d_comp_in_one_undoable_step() {
|
||||
let mut h = harness();
|
||||
assert_eq!(renderer(&h), Renderer::Classic3D);
|
||||
let steps = h.state().session.history.undo.len();
|
||||
create_ring(&mut h);
|
||||
let ls = layers(&h);
|
||||
assert_eq!(ls.len(), 1);
|
||||
assert_ring(&ls[0]);
|
||||
assert_eq!(renderer(&h), Renderer::Advanced3D, "the comp switches to Advanced 3D");
|
||||
assert_eq!(h.state().session.history.undo.len(), steps + 1);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(layers(&h).is_empty());
|
||||
assert_eq!(renderer(&h), Renderer::Classic3D, "undo restores the renderer too");
|
||||
h.state_mut().session.execute("edit.redo", json!({})).unwrap();
|
||||
assert_ring(&layers(&h)[0]);
|
||||
assert_eq!(renderer(&h), Renderer::Advanced3D);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_creates_nothing() {
|
||||
let mut h = harness();
|
||||
let steps = h.state().session.history.undo.len();
|
||||
open(&mut h);
|
||||
choose(&mut h, PrimitiveKind::Sphere);
|
||||
type_into(&mut h, "dialog.primitive.radius", "80");
|
||||
click(&mut h, "dialog.primitive.cancel");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert!(layers(&h).is_empty());
|
||||
assert_eq!(renderer(&h), Renderer::Classic3D);
|
||||
assert_eq!(h.state().session.history.undo.len(), steps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_type_offers_the_dimensions_its_layer_has() {
|
||||
let mut h = harness();
|
||||
for kind in PrimitiveKind::ALL {
|
||||
open(&mut h);
|
||||
choose(&mut h, kind);
|
||||
click(&mut h, "dialog.primitive.ok");
|
||||
let l = layers(&h).into_iter().find(|l| l.source == LayerSource::Primitive { kind }).unwrap();
|
||||
assert_eq!(l.name, kind.label(), "the name follows the type until typed");
|
||||
let group = l.props.group("geometryOptions").unwrap();
|
||||
let ids: Vec<&str> = group.props().map(|p| p.match_id.as_str()).collect();
|
||||
open(&mut h);
|
||||
choose(&mut h, kind);
|
||||
let controls = ["type", "name", "ok", "cancel"];
|
||||
let shown: Vec<&str> = (h.state().auto.elements.iter())
|
||||
.filter_map(|e| e.id.strip_prefix("dialog.primitive."))
|
||||
.filter(|id| !controls.contains(id) && !id.starts_with("typeItem."))
|
||||
.collect();
|
||||
assert_eq!(shown, ids, "{} dimensions", kind.label());
|
||||
click(&mut h, "dialog.primitive.cancel");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_created_primitive_survives_save_and_reopen() {
|
||||
let mut h = harness();
|
||||
create_ring(&mut h);
|
||||
let path = std::env::temp_dir().join(format!("ec-ui-new-primitive-{}.ecproj", std::process::id()));
|
||||
h.state_mut().session.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let mut o = Session::default();
|
||||
o.execute("file.open", json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let (_, comp) = o.project.comps().next().unwrap();
|
||||
assert_eq!(comp.renderer, Renderer::Advanced3D);
|
||||
assert_ring(&comp.layers[0]);
|
||||
}
|
||||
Reference in New Issue
Block a user