mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Merge #2050: 3D: add a New 3D Primitive dialog to Layer > New (@tylerjenningsw)
This commit is contained in:
@@ -733,6 +733,7 @@ Layer
|
||||
Shape Layer | layer.newShape
|
||||
Adjustment Layer | layer.newAdjustment
|
||||
Content-Aware Fill Layer... | layer.newContentAwareFill
|
||||
3D Primitive... | layer.newPrimitive
|
||||
Cube | layer.new3dPrimitive {"kind":"cube"}
|
||||
Sphere | layer.new3dPrimitive {"kind":"sphere"}
|
||||
Plane | layer.new3dPrimitive {"kind":"plane"}
|
||||
|
||||
@@ -588,6 +588,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "形状图层"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "调整图层"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "内容识别填充图层..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D 基本体..."),
|
||||
("layer.new3dPrimitive", "Cube", "立方体"),
|
||||
("layer.new3dPrimitive", "Sphere", "球体"),
|
||||
("layer.new3dPrimitive", "Plane", "平面"),
|
||||
@@ -1222,6 +1223,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "形狀圖層"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "調整圖層"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "內容感知填滿圖層..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D 基本體..."),
|
||||
("layer.new3dPrimitive", "Cube", "立方體"),
|
||||
("layer.new3dPrimitive", "Sphere", "球體"),
|
||||
("layer.new3dPrimitive", "Plane", "平面"),
|
||||
@@ -1853,6 +1855,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "シェイプレイヤー"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "調整レイヤー"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "コンテンツに応じた塗りつぶしレイヤー..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D プリミティブ..."),
|
||||
("layer.new3dPrimitive", "Cube", "立方体"),
|
||||
("layer.new3dPrimitive", "Sphere", "球体"),
|
||||
("layer.new3dPrimitive", "Plane", "平面"),
|
||||
@@ -2487,6 +2490,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Шар фігури"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Коригувальний шар"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Шар заливки з урахуванням вмісту..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D-примітив..."),
|
||||
("layer.new3dPrimitive", "Cube", "Куб"),
|
||||
("layer.new3dPrimitive", "Sphere", "Сфера"),
|
||||
("layer.new3dPrimitive", "Plane", "Площина"),
|
||||
@@ -3122,6 +3126,7 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Capa de forma"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Capa de ajuste"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Capa de relleno según el contenido..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "Primitiva 3D..."),
|
||||
("layer.new3dPrimitive", "Cube", "Cubo"),
|
||||
("layer.new3dPrimitive", "Sphere", "Esfera"),
|
||||
("layer.new3dPrimitive", "Plane", "Plano"),
|
||||
@@ -3755,6 +3760,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Camada de forma"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Camada de ajuste"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Camada de preenchimento sensível ao conteúdo..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "Primitiva 3D..."),
|
||||
("layer.new3dPrimitive", "Cube", "Cubo"),
|
||||
("layer.new3dPrimitive", "Sphere", "Esfera"),
|
||||
("layer.new3dPrimitive", "Plane", "Plano"),
|
||||
@@ -4395,6 +4401,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Слой-фигура"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Корректирующий слой"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Слой заливки с учетом содержимого..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D-примитив..."),
|
||||
("layer.new3dPrimitive", "Cube", "Куб"),
|
||||
("layer.new3dPrimitive", "Sphere", "Сфера"),
|
||||
("layer.new3dPrimitive", "Plane", "Плоскость"),
|
||||
@@ -5028,6 +5035,7 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Calque de forme"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Calque de réglage"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Calque de remplissage d’après le contenu..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "Primitive 3D..."),
|
||||
("layer.new3dPrimitive", "Cube", "Cube"),
|
||||
("layer.new3dPrimitive", "Sphere", "Sphère"),
|
||||
("layer.new3dPrimitive", "Plane", "Plan"),
|
||||
@@ -5661,6 +5669,7 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Formebene"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Einstellungsebene"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Ebene für inhaltsbasierte Füllung..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D-Grundkörper..."),
|
||||
("layer.new3dPrimitive", "Cube", "Würfel"),
|
||||
("layer.new3dPrimitive", "Sphere", "Kugel"),
|
||||
("layer.new3dPrimitive", "Plane", "Fläche"),
|
||||
@@ -6295,6 +6304,7 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("layer.newShape", "Shape Layer", "Vrstva tvaru"),
|
||||
("layer.newAdjustment", "Adjustment Layer", "Vrstva úprav"),
|
||||
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Vrstva výplně podle obsahu..."),
|
||||
("layer.newPrimitive", "3D Primitive...", "3D primitivum..."),
|
||||
("layer.new3dPrimitive", "Cube", "Krychle"),
|
||||
("layer.new3dPrimitive", "Sphere", "Koule"),
|
||||
("layer.new3dPrimitive", "Plane", "Rovina"),
|
||||
|
||||
@@ -72,6 +72,7 @@ pub enum Dialog {
|
||||
ViewOptions,
|
||||
CameraSettings,
|
||||
LightSettings,
|
||||
NewPrimitive,
|
||||
KeyVelocity,
|
||||
KeyInterpolation,
|
||||
TimeStretch,
|
||||
|
||||
@@ -399,6 +399,10 @@ pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: V
|
||||
if crate::panels::dialogs_3d::route(app, id, ¶ms)? {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
// New 3D Primitive without parameters opens its dialog.
|
||||
if crate::panels::primitive_dialog::route(app, id, ¶ms)? {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
// Layer Settings on a solid, adjustment layer or null opens its settings dialog.
|
||||
if crate::panels::dialogs::route_layer_settings(app, id, ¶ms)? {
|
||||
return Ok(Value::Null);
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct DialogState {
|
||||
pub info: (String, String),
|
||||
pub camera: super::dialogs_3d::CameraDraft,
|
||||
pub light: super::dialogs_3d::LightDraft,
|
||||
pub primitive: super::primitive_dialog::PrimitiveDraft,
|
||||
pub velocity: super::key_dialogs::VelocityDraft,
|
||||
pub interp: super::key_dialogs::InterpDraft,
|
||||
pub stretch: super::key_dialogs::StretchDraft,
|
||||
@@ -248,6 +249,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
Dialog::ViewOptions => super::forms::show_view_options(app, ctx, &t),
|
||||
Dialog::CameraSettings => super::dialogs_3d::camera(app, ctx, &t),
|
||||
Dialog::LightSettings => super::dialogs_3d::light(app, ctx, &t),
|
||||
Dialog::NewPrimitive => super::primitive_dialog::dialog(app, ctx, &t),
|
||||
Dialog::KeyVelocity => super::key_dialogs::velocity(app, ctx, &t),
|
||||
Dialog::KeyInterpolation => super::key_dialogs::interpolation(app, ctx, &t),
|
||||
Dialog::TimeStretch => super::key_dialogs::time_stretch(app, ctx, &t),
|
||||
|
||||
@@ -35,6 +35,7 @@ pub mod paint_panels;
|
||||
pub mod panel_kit;
|
||||
pub mod path_vr_panels;
|
||||
pub mod precomp;
|
||||
pub mod primitive_dialog;
|
||||
pub mod project;
|
||||
pub mod properties;
|
||||
pub mod puppet_tool;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! New 3D Primitive dialog (Layer ▸ New ▸ 3D Primitive…): the primitive's type, name and
|
||||
//! dimensions. OK runs `layer.newPrimitive`, which switches a Classic 3D comp to Advanced 3D;
|
||||
//! the layer's Geometry Options edit it afterwards.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::i18n::tr;
|
||||
use effectcraft_engine::project::build::{Ids, primitive_geometry_options};
|
||||
use effectcraft_engine::project::{ItemId, ParamUi, PrimitiveKind, Property};
|
||||
use egui::{Color32, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::theme::Tokens;
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PrimitiveDraft {
|
||||
pub kind: PrimitiveKind,
|
||||
pub name: String,
|
||||
/// Whether the name was typed; until then it follows the type.
|
||||
pub named: bool,
|
||||
/// Dimensions typed, by Geometry Options match id, kept when the type changes (a sphere's
|
||||
/// radius carries over to a cone); the rest are the type's defaults.
|
||||
pub dims: BTreeMap<String, f64>,
|
||||
/// The project (its epoch) and comp the dialog was opened for.
|
||||
pub target: Option<(u64, ItemId)>,
|
||||
}
|
||||
|
||||
fn target(app: &EffectcraftApp) -> Option<(u64, ItemId)> {
|
||||
Some((app.session.script_ui.project_epoch, app.session.active_comp_id()?))
|
||||
}
|
||||
|
||||
/// The dimensions a primitive type has: its Geometry Options as the engine builds them.
|
||||
fn dimensions(kind: PrimitiveKind) -> Vec<Property> {
|
||||
let mut next = 0;
|
||||
primitive_geometry_options(&mut Ids(&mut next), kind).props().cloned().collect()
|
||||
}
|
||||
|
||||
/// Open the dialog for a new primitive in the active comp.
|
||||
pub fn open(app: &mut EffectcraftApp) -> Result<(), String> {
|
||||
let target = Some(target(app).ok_or("no composition is open")?);
|
||||
let kind = PrimitiveKind::Cube;
|
||||
app.dialog_state.primitive = PrimitiveDraft { kind, name: kind.label().to_string(), named: false, dims: BTreeMap::new(), target };
|
||||
app.dialog = Some(Dialog::NewPrimitive);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let mut d = app.dialog_state.primitive.clone();
|
||||
// A project opened or another comp made active since (by a script or the control channel):
|
||||
// the primitive would land somewhere the dialog was not opened for.
|
||||
if target(app) != d.target {
|
||||
app.ui.status = "New 3D Primitive closed: the composition changed".into();
|
||||
app.dialog = None;
|
||||
return;
|
||||
}
|
||||
let (mut ok, mut close) = (false, false);
|
||||
super::dialogs::modal(ctx, "New 3D Primitive", vec2(420.0, 300.0), t, |ui| {
|
||||
egui::Grid::new("primitive-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Type"));
|
||||
let combo = egui::ComboBox::from_id_salt("primitive-type").selected_text(tr(d.kind.label())).show_ui(ui, |ui| {
|
||||
for (i, k) in PrimitiveKind::ALL.into_iter().enumerate() {
|
||||
let item = ui.selectable_value(&mut d.kind, k, tr(k.label()));
|
||||
app.auto.add(&format!("dialog.primitive.typeItem.{i}"), item.rect, k.label());
|
||||
}
|
||||
});
|
||||
app.auto.add_response("dialog.primitive.type", &combo.response, "Type");
|
||||
if !d.named {
|
||||
d.name = d.kind.label().to_string();
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label(tr("Name"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
|
||||
d.named |= r.changed();
|
||||
app.auto.add_response("dialog.primitive.name", &r, "Name");
|
||||
ui.end_row();
|
||||
for p in dimensions(d.kind) {
|
||||
let ParamUi::Slider { min, max, decimals, .. } = p.ui else { continue };
|
||||
let mut v = d.dims.get(&p.match_id).copied().unwrap_or(p.value.as_f64());
|
||||
ui.label(tr(&p.name));
|
||||
// Counts (segments, rings) are whole numbers; sizes are pixels.
|
||||
let suffix = if decimals == 0 { "" } else { " px" };
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut v).range(min..=max).max_decimals(decimals as usize).suffix(suffix)));
|
||||
app.auto.add_response(&format!("dialog.primitive.{}", p.match_id), &r, &p.name);
|
||||
if r.changed() {
|
||||
d.dims.insert(p.match_id, v);
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
|
||||
app.auto.add("dialog.primitive.ok", b.rect, "OK");
|
||||
ok |= b.clicked();
|
||||
let c = ui.button(tr("Cancel"));
|
||||
app.auto.add("dialog.primitive.cancel", c.rect, "Cancel");
|
||||
close |= c.clicked();
|
||||
});
|
||||
});
|
||||
app.dialog_state.primitive = d.clone();
|
||||
if ok || super::dialogs::ui_enter(ctx) {
|
||||
let mut p = json!({"kind": d.kind.label().to_ascii_lowercase(), "name": d.name});
|
||||
for dim in dimensions(d.kind) {
|
||||
if let Some(v) = d.dims.get(&dim.match_id) {
|
||||
p[dim.match_id.as_str()] = json!(v);
|
||||
}
|
||||
}
|
||||
if let Err(e) = app.session.execute("layer.newPrimitive", p) {
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu routing: New 3D Primitive without parameters opens the dialog. Returns true when it did.
|
||||
pub fn route(app: &mut EffectcraftApp, id: &str, params: &Value) -> Result<bool, String> {
|
||||
if id != "layer.newPrimitive" || !params.as_object().is_none_or(|m| m.is_empty()) {
|
||||
return Ok(false);
|
||||
}
|
||||
open(app).map(|_| true)
|
||||
}
|
||||
@@ -39,6 +39,7 @@ mod ui_locked_color;
|
||||
mod ui_media_browser_cache;
|
||||
mod ui_media_browser_copy_path;
|
||||
mod ui_menus;
|
||||
mod ui_new_primitive;
|
||||
mod ui_object_matte;
|
||||
mod ui_panel_focus;
|
||||
mod ui_panel_windows;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
//! 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]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untouched_dimensions_take_each_types_defaults_and_typed_ones_carry_over() {
|
||||
let mut h = harness();
|
||||
open(&mut h);
|
||||
choose(&mut h, PrimitiveKind::Plane);
|
||||
click(&mut h, "dialog.primitive.ok");
|
||||
let plane = layers(&h).remove(0);
|
||||
assert_eq!(geometry(&plane, "width"), 400.0, "a plane's default, not the cube's 300");
|
||||
open(&mut h);
|
||||
type_into(&mut h, "dialog.primitive.width", "120");
|
||||
choose(&mut h, PrimitiveKind::Plane);
|
||||
click(&mut h, "dialog.primitive.ok");
|
||||
let plane = layers(&h).into_iter().find(|l| l.id != plane.id).unwrap();
|
||||
assert_eq!(geometry(&plane, "width"), 120.0, "the typed width carries over");
|
||||
assert_eq!(geometry(&plane, "height"), 400.0);
|
||||
}
|
||||
|
||||
/// A dialog left open while something else changes the editing context closes without creating
|
||||
/// anything rather than adding the primitive where it was not opened.
|
||||
fn closes_when(change: impl FnOnce(&mut Session)) {
|
||||
let mut h = harness();
|
||||
let a = h.state().session.active_comp_id().unwrap();
|
||||
open(&mut h);
|
||||
choose(&mut h, PrimitiveKind::Torus);
|
||||
change(&mut h.state_mut().session);
|
||||
let b = h.state().session.active_comp_id().unwrap();
|
||||
let steps = h.state().session.history.undo.len();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, None, "the dialog closes");
|
||||
assert!(rect(&h, "dialog.primitive.ok").is_none());
|
||||
let s = &h.state().session;
|
||||
for c in [a, b] {
|
||||
if let Some(c) = s.project.comp(c) {
|
||||
assert!(c.layers.is_empty(), "nothing is created");
|
||||
assert_eq!(c.renderer, Renderer::Classic3D);
|
||||
}
|
||||
}
|
||||
assert_eq!(s.history.undo.len(), steps);
|
||||
assert!(h.state().ui.status.contains("changed"), "{}", h.state().ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_comp_made_active_closes_the_dialog() {
|
||||
closes_when(|s| {
|
||||
s.execute("comp.new", json!({"name": "Other", "width": 640, "height": 360, "duration": 2})).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_replaced_project_closes_the_dialog_even_when_the_comp_id_is_reused() {
|
||||
closes_when(|s| {
|
||||
let before = s.active_comp_id().unwrap();
|
||||
s.execute("file.newProject", json!({})).unwrap();
|
||||
s.execute("comp.new", json!({"name": "Scene", "width": 640, "height": 360, "duration": 2})).unwrap();
|
||||
assert_eq!(s.active_comp_id(), Some(before), "the new project reuses the comp's id");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user