3D: Layer > New > 3D Primitive... dialog for a primitive's type, name and dimensions (#1734)

OK runs layer.newPrimitive, which switches a Classic 3D comp to Advanced 3D in the same undo step. The dimension fields are the Geometry Options the engine builds for the chosen type.
This commit is contained in:
tylerjenningsw
2026-10-21 13:31:58 -04:00
parent 9db7f3b217
commit f14a57fb3e
7 changed files with 129 additions and 0 deletions
+1
View File
@@ -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"}
+10
View File
@@ -505,6 +505,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", "平面"),
@@ -1139,6 +1140,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", "平面"),
@@ -1770,6 +1772,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", "平面"),
@@ -2404,6 +2407,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", "Площина"),
@@ -3039,6 +3043,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"),
@@ -3672,6 +3677,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"),
@@ -4312,6 +4318,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", "Плоскость"),
@@ -4945,6 +4952,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"),
@@ -5578,6 +5586,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"),
@@ -6212,6 +6221,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"),
+2
View File
@@ -72,6 +72,8 @@ pub enum Dialog {
ViewOptions,
CameraSettings,
LightSettings,
/// Layer ▸ New ▸ 3D Primitive… (`panels::primitive_dialog`).
NewPrimitive,
KeyVelocity,
KeyInterpolation,
TimeStretch,
+4
View File
@@ -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, &params)? {
return Ok(Value::Null);
}
// New 3D Primitive without parameters opens its dialog.
if crate::panels::primitive_dialog::route(app, id, &params)? {
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, &params)? {
return Ok(Value::Null);
+2
View File
@@ -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),
+1
View File
@@ -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,109 @@
//! 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::{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 by Geometry Options match id, kept when the type changes (a sphere's radius
/// carries over to a cone).
pub dims: BTreeMap<String, f64>,
}
/// 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> {
app.session.active_comp().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() };
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();
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 v = d.dims.entry(p.match_id.clone()).or_insert(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(v).range(min..=max).max_decimals(decimals as usize).suffix(suffix)));
app.auto.add_response(&format!("dialog.primitive.{}", p.match_id), &r, &p.name);
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)
}