mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 07:45:38 +00:00
Merge pull request #6153 from dotCooCoo/feat/three-d-panel
3D and Materials: Window › 3D and Materials edits the selection's 3D effect in place
This commit is contained in:
@@ -84,8 +84,8 @@ pub use reshape::{
|
||||
};
|
||||
pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_options, validate_revolve};
|
||||
pub use solids::{
|
||||
EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SOLIDS, extrude_options, has_solid, inflate_options, is_three_d, solid_art, validate_solid,
|
||||
validate_three_d, view_options,
|
||||
EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SOLIDS, THREE_D_TYPES, extrude_options, has_solid, inflate_options, is_three_d, solid_art,
|
||||
three_d_params_as, validate_solid, validate_three_d, view_options,
|
||||
};
|
||||
pub use stylize::{ScribbleSetting, scribble_setting_id, scribble_settings};
|
||||
pub use trap::TRAP;
|
||||
|
||||
@@ -25,6 +25,32 @@ pub fn is_three_d(id: &str) -> bool {
|
||||
id == crate::REVOLVE || SOLIDS.contains(&id)
|
||||
}
|
||||
|
||||
/// The 3D and Materials panel's object types and the effect that draws each: (type, effect id).
|
||||
/// Plane keeps the art flat, which is what Rotate draws.
|
||||
pub const THREE_D_TYPES: [(&str, &str); 4] = [("plane", ROTATE), ("extrude", EXTRUDE), ("revolve", crate::REVOLVE), ("inflate", INFLATE)];
|
||||
|
||||
/// The parameters of 3D effect `to` for an effect `from` with `params`, as switching the object
|
||||
/// type keeps them: every setting `to` takes keeps its value from `params` (the defaults of `from`
|
||||
/// under them), each rotation angle wrapped into -180° to 180° when `to` is not Revolve, and the
|
||||
/// settings only `to` takes start at their defaults. Settings `to` does not take are dropped.
|
||||
pub fn three_d_params_as(from: &str, params: &Value, to: &str) -> Value {
|
||||
let current = crate::merged_params(from, params);
|
||||
let mut out = crate::default_params(to).and_then(|v| v.as_object().cloned()).unwrap_or_default();
|
||||
for (key, value) in out.iter_mut() {
|
||||
if let Some(v) = current.get(key) {
|
||||
value.clone_from(v);
|
||||
}
|
||||
}
|
||||
if to != crate::REVOLVE {
|
||||
for key in ["rotationX", "rotationY", "rotationZ"] {
|
||||
if let Some(a) = out.get(key).and_then(Value::as_f64).filter(|a| !(-180.0..=180.0).contains(a)) {
|
||||
out.insert(key.into(), serde_json::json!((a + 180.0).rem_euclid(360.0) - 180.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Object(out)
|
||||
}
|
||||
|
||||
fn solid(fx: &[Effect]) -> Option<(usize, &Effect)> {
|
||||
fx.iter().enumerate().find(|(_, e)| e.visible && SOLIDS.contains(&e.id.as_str()))
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Effect Options",
|
||||
[],
|
||||
None,
|
||||
"{index: int, params?: {…} (merged into the current parameters), warpStyle?: string (an existing Warp style suffix, e.g. arc or flag; changes only an applied Warp in place), visible?: bool, item?: appearance item index|null (as effect.remove), ids?|id?: [..], target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} → {ids}",
|
||||
"{index: int, params?: {…} (merged into the current parameters), warpStyle?: string (an existing Warp style suffix, e.g. arc or flag; changes only an applied Warp in place), threeDType?: plane|extrude|revolve|inflate (the 3D and Materials panel's object type: changes only an applied 3D effect in place into Rotate, Extrude & Bevel, Revolve or Inflate; the settings the new effect also takes keep their values, rotation angles wrapped into -180..180 outside Revolve, and the others start at its defaults; params then merge into the new effect's; the type the effect already has, with nothing else given, changes nothing and records no undo step), visible?: bool, item?: appearance item index|null (as effect.remove), ids?|id?: [..], target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} → {ids}",
|
||||
has_doc,
|
||||
set_params
|
||||
),
|
||||
@@ -422,10 +422,28 @@ pub(crate) fn set_params_effects(s: &Session, p: &Value) -> Vec<String> {
|
||||
return vec![];
|
||||
};
|
||||
let Ok(targets) = effect_targets(s, p, item, index, C) else { return vec![] };
|
||||
let effect = |&(id, at, index): &(NodeId, Option<usize>, usize)| Some(st.doc.node(id)?.appearance.effects_at(at)?.get(index)?.id.clone());
|
||||
// A 3D effect whose type `threeDType` changes takes the new effect's parameters.
|
||||
let kind = three_d_type(p, C).ok().flatten();
|
||||
let effect = |&(id, at, index): &(NodeId, Option<usize>, usize)| {
|
||||
let id = &st.doc.node(id)?.appearance.effects_at(at)?.get(index)?.id;
|
||||
Some(kind.filter(|_| effects::is_three_d(id)).map_or_else(|| id.clone(), str::to_string))
|
||||
};
|
||||
targets.iter().filter_map(effect).collect()
|
||||
}
|
||||
|
||||
/// The effect `threeDType` in `p` turns a 3D effect into ([`effects::THREE_D_TYPES`]), when given.
|
||||
fn three_d_type(p: &Value, cmd: &str) -> Result<Option<&'static str>> {
|
||||
let key = "threeDType";
|
||||
let Some(v) = p.get(key) else { return Ok(None) };
|
||||
match v.as_str().and_then(|t| effects::THREE_D_TYPES.iter().find(|(name, _)| *name == t)) {
|
||||
Some((_, id)) => Ok(Some(id)),
|
||||
None => {
|
||||
let names: Vec<&str> = effects::THREE_D_TYPES.iter().map(|(name, _)| *name).collect();
|
||||
Err(bad(cmd, format!("`{key}` must be one of {}", names.join(", "))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lighting_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.lightingPreset";
|
||||
let patch = str_param(p, "preset")
|
||||
@@ -450,8 +468,9 @@ fn lighting_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// `effect.setParams`: merges `params` into the effect at `index` of the targets' active item, and
|
||||
/// sets its Warp style (`warpStyle`) or visibility (`visible`), as one undo step. They are checked
|
||||
/// as `effect.apply` checks them, and a visible 3D effect must still fit. → `{ids}`.
|
||||
/// sets its Warp style (`warpStyle`), its 3D type (`threeDType`) or visibility (`visible`), as one
|
||||
/// undo step. They are checked as `effect.apply` checks them, and a visible 3D effect must still
|
||||
/// fit. → `{ids}`.
|
||||
fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.setParams";
|
||||
let index = index_param(p, "index", C)?;
|
||||
@@ -459,6 +478,7 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !params.is_null() && !params.is_object() {
|
||||
return Err(bad(C, "`params` must be an object"));
|
||||
}
|
||||
let kind = three_d_type(p, C)?;
|
||||
let warp_id = p
|
||||
.get("warpStyle")
|
||||
.map(|v| -> Result<String> {
|
||||
@@ -476,21 +496,41 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if edited.iter().any(|e| texture_load_effect(e)) {
|
||||
load_texture(&mut params, C)?;
|
||||
}
|
||||
for (id, target, index) in effect_targets(s, p, item, index, C)? {
|
||||
let targets = effect_targets(s, p, item, index, C)?;
|
||||
// The type each 3D effect already has, with nothing else to change, is no edit (and is not
|
||||
// checked again).
|
||||
let nothing_else = params.as_object().is_none_or(serde_json::Map::is_empty) && visible.is_none() && warp_id.is_none();
|
||||
if let Some(to) = kind
|
||||
&& nothing_else
|
||||
{
|
||||
let d = &s.doc()?.doc;
|
||||
// The effects edited: a target without an effect at `index` is left as it is.
|
||||
let ids: Vec<&str> =
|
||||
targets.iter().filter_map(|&(id, at, index)| d.node(id)?.appearance.effects_at(at)?.get(index).map(|e| e.id.as_str())).collect();
|
||||
if !ids.is_empty() && ids.iter().all(|id| *id == to) {
|
||||
return Ok(json!({ "ids": [] }));
|
||||
}
|
||||
}
|
||||
for &(id, target, index) in &targets {
|
||||
let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;
|
||||
let effect = n.appearance.effects_at(target).and_then(|fx| fx.get(index));
|
||||
if warp_id.is_some() && effect.is_some_and(|e| e.id.strip_prefix("warp.").and_then(effects::WarpStyle::from_id).is_none()) {
|
||||
return Err(bad(C, "warpStyle can only edit an applied Warp effect"));
|
||||
}
|
||||
if kind.is_some() && effect.is_some_and(|e| !effects::is_three_d(&e.id)) {
|
||||
return Err(bad(C, format!("object {} needs a 3D effect at index {index}", id.0)));
|
||||
}
|
||||
if let Some(e) = effect
|
||||
&& effects::is_three_d(&e.id)
|
||||
&& visible != Some(false)
|
||||
{
|
||||
let mut prospective = effects::merged_params(&e.id, &e.params);
|
||||
if let (Some(cur), Value::Object(new)) = (prospective.as_object_mut(), effects::upgrade_params(&e.id, ¶ms)) {
|
||||
let to = kind.unwrap_or(&e.id);
|
||||
let mut prospective =
|
||||
if to == e.id { effects::merged_params(&e.id, &e.params) } else { effects::three_d_params_as(&e.id, &e.params, to) };
|
||||
if let (Some(cur), Value::Object(new)) = (prospective.as_object_mut(), effects::upgrade_params(to, ¶ms)) {
|
||||
cur.extend(new);
|
||||
}
|
||||
effects::validate_three_d(n, &e.id, &prospective).map_err(|e| bad(C, e))?;
|
||||
effects::validate_three_d(n, to, &prospective).map_err(|e| bad(C, e))?;
|
||||
}
|
||||
}
|
||||
let ids = edit_effects(s, p, item, C, "Effect Options", &format!("{C}: no effect at index {index}"), index, |fx, index| {
|
||||
@@ -498,6 +538,12 @@ fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(id) = &warp_id {
|
||||
e.id.clone_from(id);
|
||||
}
|
||||
if let Some(to) = kind
|
||||
&& e.id != to
|
||||
{
|
||||
e.params = effects::three_d_params_as(&e.id, &e.params, to);
|
||||
e.id = to.to_string();
|
||||
}
|
||||
if let (Value::Object(new), cur) = (effects::upgrade_params(&e.id, ¶ms), &mut e.params) {
|
||||
if !cur.is_object() {
|
||||
*cur = effects::merged_params(&e.id, &Value::Null);
|
||||
@@ -859,5 +905,8 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
#[cfg(test)]
|
||||
mod lighting_presets_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod three_d_type_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#![allow(clippy::unwrap_used, clippy::panic)]
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn setup(effect: &str, params: Value) -> (Session, NodeId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width":400,"height":400})).unwrap();
|
||||
let id = NodeId(s.execute("path.create", &json!({"d":"M100 100L200 100L200 200L100 200Z"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color":"#338ad6"})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect":effect,"item":null,"params":params})).unwrap();
|
||||
(s, id)
|
||||
}
|
||||
|
||||
fn effect(s: &Session, id: NodeId) -> (String, Value) {
|
||||
let e = &s.doc().unwrap().doc.node(id).unwrap().appearance.effects[0];
|
||||
(e.id.clone(), e.params.clone())
|
||||
}
|
||||
|
||||
fn undo_len(s: &Session) -> usize {
|
||||
s.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_d_type_turns_the_effect_in_place_keeping_shared_settings_in_one_undo_step() {
|
||||
let types = [("plane", "threeD.rotate"), ("extrude", "threeD.extrude"), ("revolve", "threeD.revolve"), ("inflate", "threeD.inflate")];
|
||||
for (from_type, from) in types {
|
||||
for (to_type, to) in types {
|
||||
if from == to {
|
||||
continue;
|
||||
}
|
||||
let (mut s, id) = setup(from, json!({"rotationX": 12.0, "rotationY": -30.0, "rotationZ": 5.0, "ambient": 40.0, "lightIntensity": 55.0}));
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let history = undo_len(&s);
|
||||
let p = json!({"index": 0, "item": null, "threeDType": to_type});
|
||||
s.check_params("effect.setParams", &p).unwrap();
|
||||
s.execute("effect.setParams", &p).unwrap();
|
||||
let (now, params) = effect(&s, id);
|
||||
assert_eq!(now, to, "{from_type} → {to_type}");
|
||||
assert_eq!(params["rotationX"], 12.0, "{from_type} → {to_type}");
|
||||
assert_eq!(params["rotationY"], -30.0);
|
||||
assert_eq!(params["ambient"], 40.0);
|
||||
assert_eq!(params["lightIntensity"], 55.0);
|
||||
// Settings only the new effect takes start at its defaults; others are dropped.
|
||||
let defaults = vectorcraft_render::effects::default_params(to).unwrap();
|
||||
for key in params.as_object().unwrap().keys() {
|
||||
assert!(defaults.get(key).is_some(), "{from_type} → {to_type} kept `{key}`");
|
||||
}
|
||||
if to == "threeD.extrude" {
|
||||
assert_eq!(params["bevel"], defaults["bevel"]);
|
||||
}
|
||||
assert_eq!(undo_len(&s), history + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_d_type_carries_depth_between_extrude_and_inflate_and_merges_params_after() {
|
||||
let (mut s, id) = setup("threeD.extrude", json!({"depth": 80.0, "twist": 30.0, "bevel": "classic"}));
|
||||
s.execute("effect.setParams", &json!({"index": 0, "threeDType": "inflate", "params": {"volume": 70.0}})).unwrap();
|
||||
let (now, params) = effect(&s, id);
|
||||
assert_eq!(now, "threeD.inflate");
|
||||
assert_eq!(params["depth"], 80.0);
|
||||
assert_eq!(params["twist"], 30.0);
|
||||
assert_eq!(params["volume"], 70.0);
|
||||
assert!(params.get("bevel").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revolve_angles_outside_half_a_turn_wrap_when_the_type_leaves_revolve() {
|
||||
let (mut s, id) = setup("threeD.revolve", json!({"rotationX": 270.0, "rotationY": -200.0}));
|
||||
s.execute("effect.setParams", &json!({"index": 0, "threeDType": "plane"})).unwrap();
|
||||
let (_, params) = effect(&s, id);
|
||||
assert_eq!(params["rotationX"], -90.0);
|
||||
assert_eq!(params["rotationY"], 160.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_current_type_alone_changes_nothing_and_records_no_undo_step() {
|
||||
let (mut s, id) = setup("threeD.extrude", json!({"depth": 80.0}));
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let history = undo_len(&s);
|
||||
let r = s.execute("effect.setParams", &json!({"index": 0, "threeDType": "extrude"})).unwrap();
|
||||
assert_eq!(r["ids"], json!([]));
|
||||
assert_eq!(undo_len(&s), history);
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
// With other changes the edit runs as usual.
|
||||
s.execute("effect.setParams", &json!({"index": 0, "threeDType": "extrude", "params": {"depth": 90.0}})).unwrap();
|
||||
assert_eq!(effect(&s, id).1["depth"], 90.0);
|
||||
assert_eq!(undo_len(&s), history + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_current_type_with_an_object_without_effects_selected_too_records_no_undo_step() {
|
||||
let (mut s, a) = setup("threeD.extrude", json!({}));
|
||||
let b = NodeId(s.execute("path.create", &json!({"d":"M250 100L350 100L350 200L250 200Z"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let history = undo_len(&s);
|
||||
s.execute("effect.setParams", &json!({"index": 0, "item": null, "aligned": true, "threeDType": "extrude"})).unwrap();
|
||||
assert_eq!(undo_len(&s), history);
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_d_type_rejects_unknown_types_and_effects_that_are_not_3d() {
|
||||
let (mut s, _) = setup("threeD.extrude", json!({}));
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
for t in [json!("cube"), json!(""), json!(null), json!(2), json!("Plane"), json!("plane/../x")] {
|
||||
let p = json!({"index": 0, "threeDType": t});
|
||||
let err = s.execute("effect.setParams", &p).unwrap_err().to_string();
|
||||
assert!(err.contains("must be one of plane, extrude, revolve, inflate"), "{t}: {err}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "item": null})).unwrap();
|
||||
let err = s.execute("effect.setParams", &json!({"index": 1, "threeDType": "plane"})).unwrap_err().to_string();
|
||||
assert!(err.contains("needs a 3D effect at index 1"), "{err}");
|
||||
let err = s.execute("effect.setParams", &json!({"index": 5, "threeDType": "plane"})).unwrap_err().to_string();
|
||||
assert!(err.contains("no effect at index 5"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_d_type_turns_every_selected_object_in_one_undo_step() {
|
||||
let (mut s, a) = setup("threeD.rotate", json!({}));
|
||||
let b = NodeId(s.execute("path.create", &json!({"d":"M250 100L350 100L350 200L250 200Z"})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("paint.setFill", &json!({"color":"#d63333"})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect":"threeD.rotate","item":null})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let history = undo_len(&s);
|
||||
s.execute("effect.setParams", &json!({"index": 0, "item": null, "aligned": true, "threeDType": "extrude"})).unwrap();
|
||||
assert_eq!(effect(&s, a).0, "threeD.extrude");
|
||||
assert_eq!(effect(&s, b).0, "threeD.extrude");
|
||||
assert_eq!(undo_len(&s), history + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn params_for_the_new_type_pass_the_parameter_check() {
|
||||
let (s, _) = setup("threeD.rotate", json!({}));
|
||||
// `depth` is an Extrude & Bevel setting, checked against the effect the type makes.
|
||||
s.check_params("effect.setParams", &json!({"index": 0, "threeDType": "extrude", "params": {"depth": 20.0}})).unwrap();
|
||||
}
|
||||
@@ -557,6 +557,8 @@ impl VectorcraftApp {
|
||||
trace_runs::show(self, &ctx);
|
||||
opening::show(self, &ctx);
|
||||
palette::show(self, &ctx);
|
||||
// After every panel had its chance to draw in this pass.
|
||||
crate::panels::three_d::end_stale_drag(self, &ctx);
|
||||
if self.custom_titlebar {
|
||||
titlebar::resize_zones(ui);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ pub mod rearrange_artboards;
|
||||
pub mod recolor;
|
||||
mod recovery;
|
||||
pub mod rename_artboards;
|
||||
mod revolve;
|
||||
pub(crate) mod revolve;
|
||||
pub(crate) mod revolve_gizmo;
|
||||
pub mod saturate;
|
||||
pub mod save_brush_library;
|
||||
|
||||
@@ -60,7 +60,10 @@ pub(super) fn body(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
}
|
||||
|
||||
fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// The tabs and controls of the 3D effect in `d.fields["__effect"]`, editing `d.fields`; the
|
||||
/// dialog's body and the 3D and Materials panel draw them. With `__panel` set (the panel), the
|
||||
/// controls that only the dialog's artboard gizmos use are left out.
|
||||
pub(crate) fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.spacing_mut().item_spacing.y = 4.0;
|
||||
let id = d.str("__effect");
|
||||
if id != REVOLVE {
|
||||
@@ -95,14 +98,13 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
number(ui, d, "offset", tl!("Offset"), (0.0, 100000.0), "", 1, Some(unit));
|
||||
ui.horizontal(|ui| {
|
||||
let mut cap = d.bool("cap");
|
||||
let cap = flag(d, "cap", true);
|
||||
let tip = tl!("Close the circular ends of open profiles. Partial sweeps keep their cut sides open.");
|
||||
if widgets::check_tip(ui, c3("Cap"), cap, true, tip) {
|
||||
cap = !cap;
|
||||
d.fields.insert("cap".into(), json!(cap));
|
||||
if widgets::check3_tip(ui, c3("Cap"), cap, true, tip) {
|
||||
d.fields.insert("cap".into(), json!(cap != Some(true)));
|
||||
}
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, "cap", !cap, "true") {
|
||||
if reset_dot(ui, "cap", cap != Some(true), "true") {
|
||||
d.fields.insert("cap".into(), json!(true));
|
||||
}
|
||||
});
|
||||
@@ -115,20 +117,22 @@ fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.add_space(6.0);
|
||||
egui::CollapsingHeader::new(tl!("Advanced")).id_salt("revolve-advanced").show(ui, |ui| {
|
||||
number(ui, d, "segments", tl!("Segments"), (8.0, 128.0), "", 0, None);
|
||||
super::form::check(ui, d, "__showAxis", tl!("Show axis"));
|
||||
if !d.bool("__panel") {
|
||||
super::form::check(ui, d, "__showAxis", tl!("Show axis"));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
let visible = d.fields.get("expandVisibleOnly").and_then(Value::as_bool).unwrap_or(true);
|
||||
let visible = flag(d, "expandVisibleOnly", true);
|
||||
let tip = tl!("Trim covered geometry on Expand Appearance. Live preview keeps the full surface. Transparent paints and surfaces exceeding visibility limits retain all faces.");
|
||||
let toggled = ui.allocate_ui_with_layout(
|
||||
vec2((ui.available_width() - 30.0).max(100.0), 24.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
widgets::check_tip(ui, tl!("Keep visible surfaces only when expanding"), visible, true, tip)
|
||||
widgets::check3_tip(ui, tl!("Keep visible surfaces only when expanding"), visible, true, tip)
|
||||
},
|
||||
).inner;
|
||||
if toggled {
|
||||
d.fields.insert("expandVisibleOnly".into(), json!(!visible));
|
||||
d.fields.insert("expandVisibleOnly".into(), json!(visible != Some(true)));
|
||||
}
|
||||
if reset_dot(ui, "expandVisibleOnly", !visible, "true") {
|
||||
if reset_dot(ui, "expandVisibleOnly", visible != Some(true), "true") {
|
||||
d.fields.insert("expandVisibleOnly".into(), json!(true));
|
||||
}
|
||||
});
|
||||
@@ -248,7 +252,9 @@ fn view_controls(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, options: &[(Value, &str)]) {
|
||||
let id = d.str("__effect");
|
||||
let default = vectorcraft_effects::default_params(&id).and_then(|p| p.get(key).cloned()).unwrap_or(Value::Null);
|
||||
let value = d.fields.get(key).cloned().unwrap_or_else(|| default.clone());
|
||||
// A mixed setting (null) shows its default, with its reset dot filled.
|
||||
let mixed = d.fields.get(key).is_some_and(Value::is_null);
|
||||
let value = d.fields.get(key).filter(|v| !v.is_null()).cloned().unwrap_or_else(|| default.clone());
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 6.0;
|
||||
label(ui, name);
|
||||
@@ -261,38 +267,48 @@ fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, options: &[(
|
||||
d.fields.insert(key.into(), v.clone());
|
||||
}
|
||||
let default_label = options.iter().find(|(v, _)| *v == default).map_or("", |(_, l)| *l);
|
||||
if reset_dot(ui, key, value != default, default_label) {
|
||||
if reset_dot(ui, key, mixed || value != default, default_label) {
|
||||
d.fields.insert(key.into(), default.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A checkbox for `key`, with its reset dot.
|
||||
/// The checkbox setting `key`: `None` when it is null (the 3D and Materials panel's selected
|
||||
/// effects hold it with different values), else its value, or `default` when it has none.
|
||||
fn flag(d: &Dialog, key: &str, default: bool) -> Option<bool> {
|
||||
match d.fields.get(key) {
|
||||
Some(Value::Null) => None,
|
||||
value => Some(value.and_then(Value::as_bool).unwrap_or(default)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A checkbox for `key`, with its reset dot. A mixed setting shows a dash, and a click turns it on.
|
||||
fn toggle(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, default: bool) {
|
||||
ui.horizontal(|ui| {
|
||||
let on = d.bool(key);
|
||||
if widgets::check(ui, name, on, ui.is_enabled()) {
|
||||
d.fields.insert(key.into(), json!(!on));
|
||||
let on = flag(d, key, default);
|
||||
if widgets::check3(ui, name, on, ui.is_enabled()) {
|
||||
d.fields.insert(key.into(), json!(on != Some(true)));
|
||||
}
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, key, on != default, if default { "true" } else { "false" }) {
|
||||
if reset_dot(ui, key, on != Some(default), if default { "true" } else { "false" }) {
|
||||
d.fields.insert(key.into(), json!(default));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn lighting_controls(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let shade = flag(d, "shade", true);
|
||||
ui.horizontal(|ui| {
|
||||
let shade = d.bool("shade");
|
||||
if widgets::check(ui, tl!("Shading"), shade, true) {
|
||||
d.fields.insert("shade".into(), json!(!shade));
|
||||
if widgets::check3(ui, tl!("Shading"), shade, true) {
|
||||
d.fields.insert("shade".into(), json!(shade != Some(true)));
|
||||
}
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, "shade", !shade, "true") {
|
||||
if reset_dot(ui, "shade", shade != Some(true), "true") {
|
||||
d.fields.insert("shade".into(), json!(true));
|
||||
}
|
||||
});
|
||||
ui.add_enabled_ui(d.bool("shade"), |ui| {
|
||||
// A mixed Shading leaves the light settings on, for the effects that shade.
|
||||
ui.add_enabled_ui(flag(d, "shade", true) != Some(false), |ui| {
|
||||
lighting_presets(ui, d);
|
||||
light_list(ui, d);
|
||||
let mut selected = selected_light(d);
|
||||
@@ -413,8 +429,10 @@ fn light_list(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
super::revolve_gizmo::clear(ui.ctx());
|
||||
}
|
||||
});
|
||||
// Add Light is off while a setting of the single light is mixed (null).
|
||||
let mixed = explicit_lights(d).is_none() && LIGHT_KEYS.iter().any(|(_, key)| d.fields.get(*key).is_some_and(Value::is_null));
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(list.len() < vectorcraft_three_d::MAX_LIGHTS, egui::Button::new(tl!("Add Light"))).clicked() {
|
||||
if ui.add_enabled(list.len() < vectorcraft_three_d::MAX_LIGHTS && !mixed, egui::Button::new(tl!("Add Light"))).clicked() {
|
||||
let mut added = selected_light(d);
|
||||
let defaults = vectorcraft_effects::default_params(&d.str("__effect")).unwrap_or(json!({}));
|
||||
for (_, key) in LIGHT_KEYS {
|
||||
@@ -458,7 +476,8 @@ fn light_color(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
ui.label(format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]));
|
||||
ui.add_space((ui.available_width() - 24.0).max(0.0));
|
||||
if reset_dot(ui, "lightColor", rgb != [255; 3], "#FFFFFF") {
|
||||
let mixed = d.fields.get("lightColor").is_some_and(Value::is_null);
|
||||
if reset_dot(ui, "lightColor", mixed || rgb != [255; 3], "#FFFFFF") {
|
||||
d.fields.insert("lightColor".into(), json!("#ffffff"));
|
||||
}
|
||||
});
|
||||
@@ -550,7 +569,9 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64,
|
||||
if let Some(n) = slider.0.map(|n| rail_min + f64::from(n) * (rail_max - rail_min)).or(slider.2) {
|
||||
next = Some(n);
|
||||
}
|
||||
if reset_dot(ui, key, (to(value) - default).abs() > 1e-6, &format!("{}{}", from(default), suffix)) {
|
||||
// A setting the 3D and Materials panel's selected effects hold with different values is null.
|
||||
let mixed = d.fields.get(key).is_some_and(Value::is_null);
|
||||
if reset_dot(ui, key, mixed || (to(value) - default).abs() > 1e-6, &format!("{}{}", from(default), suffix)) {
|
||||
next = Some(from(default));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
3 Color Job… Задача с 3 цвята…
|
||||
30° Angles Ъгли от 30°
|
||||
3D and Materials 3D и материали
|
||||
Plane Равнина
|
||||
Extrude Избутване
|
||||
Revolve Завъртане около ос
|
||||
Inflate Издуване
|
||||
Select objects, then pick an object type to give them a 3D effect. Изберете обекти, след това изберете тип обект, за да им придадете 3D ефект.
|
||||
3D Ribbon 3D лента
|
||||
45° Angles Ъгли от 45°
|
||||
60° Angles Ъгли от 60°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -448,6 +448,11 @@
|
||||
Document Raster Effects Settings… Postavke rasterskih efekata dokumenta…
|
||||
Vector Effects Vektorski efekti
|
||||
3D and Materials 3D i materijali
|
||||
Plane Ravan
|
||||
Extrude Istiskivanje
|
||||
Revolve Rotaciono tijelo
|
||||
Inflate Napuhni
|
||||
Select objects, then pick an object type to give them a 3D effect. Odaberite objekte, zatim odaberite vrstu objekta da im date 3D efekat.
|
||||
Extrude & Bevel… Istiskivanje i zakošenje…
|
||||
Revolve… Rotaciono tijelo…
|
||||
Inflate… Napuhni…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -107,6 +107,11 @@
|
||||
32 bit 32 bits
|
||||
32 bits/pixel 32 bits/píxel
|
||||
3D and Materials 3D i materials
|
||||
Plane Pla
|
||||
Extrude Extrusió
|
||||
Revolve Revolució
|
||||
Inflate Infla
|
||||
Select objects, then pick an object type to give them a 3D effect. Seleccioneu objectes i després trieu un tipus d’objecte per donar-los un efecte 3D.
|
||||
3D Lighting Preset Preajust d’il·luminació 3D
|
||||
3D Ribbon Cinta 3D
|
||||
4 bit 4 bits
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -456,6 +456,11 @@
|
||||
Document Raster Effects Settings… Nastavení rastrových efektů dokumentu…
|
||||
Vector Effects Vektorové efekty
|
||||
3D and Materials 3D a materiály
|
||||
Plane Rovina
|
||||
Extrude Vysunutí
|
||||
Revolve Rotační těleso
|
||||
Inflate Nafouknutí
|
||||
Select objects, then pick an object type to give them a 3D effect. Vyberte objekty a pak zvolte typ objektu, abyste jim dali 3D efekt.
|
||||
Extrude & Bevel… Vysunutí a zkosení…
|
||||
Revolve… Rotační těleso…
|
||||
Inflate… Nafouknutí…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -414,6 +414,11 @@
|
||||
Document Raster Effects Settings… Indstillinger for rastereffekter i dokumentet…
|
||||
Vector Effects Vektoreffekter
|
||||
3D and Materials 3D og materialer
|
||||
Plane Plan
|
||||
Extrude Ekstrudér
|
||||
Revolve Omdrej
|
||||
Inflate Pust op
|
||||
Select objects, then pick an object type to give them a 3D effect. Markér objekter, og vælg derefter en objekttype for at give dem en 3D-effekt.
|
||||
Extrude & Bevel… Ekstrudér og facet…
|
||||
Revolve… Omdrej…
|
||||
Inflate… Pust op…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -59,6 +59,11 @@
|
||||
3 Color Job… 3-Farben-Auftrag…
|
||||
30° Angles 30°-Winkel
|
||||
3D and Materials 3D und Materialien
|
||||
Plane Ebene
|
||||
Extrude Extrudieren
|
||||
Revolve Kreiseln
|
||||
Inflate Aufblasen
|
||||
Select objects, then pick an object type to give them a 3D effect. Wählen Sie Objekte aus und dann einen Objekttyp, um ihnen einen 3D-Effekt zu geben.
|
||||
3D Ribbon 3D-Band
|
||||
45° Angles 45°-Winkel
|
||||
60° Angles 60°-Winkel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -66,6 +66,11 @@
|
||||
3 Color Job… Εργασία τριών χρωμάτων…
|
||||
30° Angles Γωνίες 30°
|
||||
3D and Materials 3D και υλικά
|
||||
Plane Επίπεδο
|
||||
Extrude Εξώθηση
|
||||
Revolve Περιστροφή γύρω από άξονα
|
||||
Inflate Φούσκωμα
|
||||
Select objects, then pick an object type to give them a 3D effect. Επιλέξτε αντικείμενα και, στη συνέχεια, έναν τύπο αντικειμένου για να τους δώσετε εφέ 3D.
|
||||
3D Ribbon Τρισδιάστατη κορδέλα
|
||||
45° Angles Γωνίες 45°
|
||||
60° Angles Γωνίες 60°
|
||||
|
||||
|
@@ -84,6 +84,11 @@
|
||||
3 Color Job… Trabajo a 3 colores…
|
||||
30° Angles Ángulos de 30°
|
||||
3D and Materials 3D y materiales
|
||||
Plane Plano
|
||||
Extrude Extrusión
|
||||
Revolve Revolución
|
||||
Inflate Inflar
|
||||
Select objects, then pick an object type to give them a 3D effect. Seleccione objetos y luego elija un tipo de objeto para darles un efecto 3D.
|
||||
3D Ribbon Cinta 3D
|
||||
45° Angles Ángulos de 45°
|
||||
60° Angles Ángulos de 60°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -453,6 +453,11 @@
|
||||
Document Raster Effects Settings… Asiakirjan rasteritehosteiden asetukset…
|
||||
Vector Effects Vektoritehosteet
|
||||
3D and Materials 3D ja materiaalit
|
||||
Plane Taso
|
||||
Extrude Pursota
|
||||
Revolve Pyöräytä
|
||||
Inflate Täytä ilmalla
|
||||
Select objects, then pick an object type to give them a 3D effect. Valitse objektit ja valitse sitten objektityyppi, niin niille tulee 3D-tehoste.
|
||||
Extrude & Bevel… Pursota ja viistä…
|
||||
Revolve… Pyöräytä…
|
||||
Inflate… Täytä ilmalla…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -85,6 +85,11 @@
|
||||
3 Color Job… Travail en 3 couleurs…
|
||||
30° Angles Angles de 30°
|
||||
3D and Materials 3D et matières
|
||||
Plane Plan
|
||||
Extrude Extrusion
|
||||
Revolve Révolution
|
||||
Inflate Gonfler
|
||||
Select objects, then pick an object type to give them a 3D effect. Sélectionnez des objets, puis choisissez un type d’objet pour leur donner un effet 3D.
|
||||
3D Ribbon Ruban 3D
|
||||
45° Angles Angles de 45°
|
||||
60° Angles Angles de 60°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -148,6 +148,11 @@
|
||||
32 bit 32 बिट
|
||||
32 bits/pixel 32 बिट्स/पिक्सेल
|
||||
3D and Materials 3D और मटीरियल्स
|
||||
Plane समतल
|
||||
Extrude एक्सट्रूड
|
||||
Revolve रिवॉल्व
|
||||
Inflate फुलाएँ
|
||||
Select objects, then pick an object type to give them a 3D effect. ऑब्जेक्ट्स सिलेक्ट करें, फिर उन्हें 3D इफ़ेक्ट देने के लिए एक ऑब्जेक्ट टाइप चुनें।
|
||||
3D Box 3D बॉक्स
|
||||
3D Box Tool 3D बॉक्स टूल
|
||||
3D Lighting Preset 3D लाइटिंग प्रीसेट
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -111,6 +111,11 @@
|
||||
32 bit 32 bit
|
||||
32 bits/pixel 32 bit/pixel
|
||||
3D and Materials 3D és anyagok
|
||||
Plane Sík
|
||||
Extrude Kihúzás
|
||||
Revolve Forgatás tengely körül
|
||||
Inflate Felfújás
|
||||
Select objects, then pick an object type to give them a 3D effect. Jelöljön ki objektumokat, majd válasszon objektumtípust, hogy 3D-effektust kapjanak.
|
||||
3D Ribbon 3D szalag
|
||||
4 bit 4 bit
|
||||
4-Up 4 nézet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -414,6 +414,11 @@
|
||||
Document Raster Effects Settings… Pengaturan efek raster dokumen…
|
||||
Vector Effects Efek vektor
|
||||
3D and Materials 3D dan material
|
||||
Plane Bidang
|
||||
Extrude Ekstrusi
|
||||
Revolve Revolusi
|
||||
Inflate Gelembungkan
|
||||
Select objects, then pick an object type to give them a 3D effect. Pilih objek, lalu pilih jenis objek untuk memberinya efek 3D.
|
||||
Extrude & Bevel… Ekstrusi & bevel…
|
||||
Revolve… Revolusi…
|
||||
Inflate… Gelembungkan…
|
||||
|
||||
|
@@ -84,6 +84,11 @@
|
||||
3 Color Job… Lavoro a 3 colori…
|
||||
30° Angles Angoli di 30°
|
||||
3D and Materials 3D e materiali
|
||||
Plane Piano
|
||||
Extrude Estrusione
|
||||
Revolve Rivoluzione
|
||||
Inflate Gonfia
|
||||
Select objects, then pick an object type to give them a 3D effect. Seleziona gli oggetti, poi scegli un tipo di oggetto per dar loro un effetto 3D.
|
||||
3D Ribbon Nastro 3D
|
||||
45° Angles Angoli di 45°
|
||||
60° Angles Angoli di 60°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -487,6 +487,11 @@ digits Malayalam マラヤーラム数字
|
||||
Document Raster Effects Settings… ドキュメントのラスタライズ効果設定…
|
||||
Vector Effects ベクター効果
|
||||
3D and Materials 3D とマテリアル
|
||||
Plane 平面
|
||||
Extrude 押し出し
|
||||
Revolve 回転体
|
||||
Inflate 膨張
|
||||
Select objects, then pick an object type to give them a 3D effect. オブジェクトを選択してから、オブジェクトの種類を選んで 3D 効果を適用してください。
|
||||
Extrude & Bevel… 押し出しとベベル…
|
||||
Revolve… 回転体…
|
||||
Inflate… 膨張…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -380,6 +380,11 @@
|
||||
Document Raster Effects Settings… დოკუმენტის რასტრული ეფექტების პარამეტრები…
|
||||
Vector Effects ვექტორული ეფექტები
|
||||
3D and Materials 3D და მასალები
|
||||
Plane სიბრტყე
|
||||
Extrude ამოწევა
|
||||
Revolve ბრუნვა
|
||||
Inflate გაბერვა
|
||||
Select objects, then pick an object type to give them a 3D effect. აირჩიეთ ობიექტები, შემდეგ აირჩიეთ ობიექტის ტიპი, რომ მათ 3D ეფექტი მიეცეთ.
|
||||
Extrude & Bevel… ამოწევა და წაკვეთა…
|
||||
Revolve… ბრუნვა…
|
||||
Inflate… გაბერვა…
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -480,6 +480,11 @@ digits Malayalam 말라얄람어
|
||||
Document Raster Effects Settings… 문서 래스터 효과 설정…
|
||||
Vector Effects 벡터 효과
|
||||
3D and Materials 3D 및 재질
|
||||
Plane 평면
|
||||
Extrude 돌출
|
||||
Revolve 회전체
|
||||
Inflate 부풀리기
|
||||
Select objects, then pick an object type to give them a 3D effect. 개체를 선택한 다음 개체 유형을 골라 3D 효과를 적용하십시오.
|
||||
Extrude & Bevel… 돌출 및 경사…
|
||||
Revolve… 회전체…
|
||||
Inflate… 부풀리기…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -428,6 +428,11 @@
|
||||
Document Raster Effects Settings… Instellingen voor documentrastereffecten…
|
||||
Vector Effects Vectoreffecten
|
||||
3D and Materials 3D en materialen
|
||||
Plane Vlak
|
||||
Extrude Extruderen
|
||||
Revolve Draaien
|
||||
Inflate Opblazen
|
||||
Select objects, then pick an object type to give them a 3D effect. Selecteer objecten en kies dan een objecttype om ze een 3D-effect te geven.
|
||||
Extrude & Bevel… Extruderen en afschuinen…
|
||||
Revolve… Draaien…
|
||||
Inflate… Opblazen…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -416,6 +416,11 @@
|
||||
Document Raster Effects Settings… Ustawienia efektów rastrowych dokumentu…
|
||||
Vector Effects Efekty wektorowe
|
||||
3D and Materials 3D i materiały
|
||||
Plane Płaszczyzna
|
||||
Extrude Wyciągnięcie
|
||||
Revolve Obrót
|
||||
Inflate Nadmuchanie
|
||||
Select objects, then pick an object type to give them a 3D effect. Zaznacz obiekty, a następnie wybierz typ obiektu, aby nadać im efekt 3D.
|
||||
Extrude & Bevel… Wyciągnięcie i fazowanie…
|
||||
Revolve… Obrót…
|
||||
Inflate… Nadmuchanie…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -96,6 +96,11 @@
|
||||
30° Angles Ângulos de 30°
|
||||
3D Ribbon Fita 3D
|
||||
3D and Materials 3D e Materiais
|
||||
Plane Plano
|
||||
Extrude Extrusão
|
||||
Revolve Revolver
|
||||
Inflate Inflar
|
||||
Select objects, then pick an object type to give them a 3D effect. Selecione objetos e escolha um tipo de objeto para dar a eles um efeito 3D.
|
||||
45° Angles Ângulos de 45°
|
||||
60° Angles Ângulos de 60°
|
||||
90° & 45° & 30° Angles Ângulos de 90° & 45° & 30°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -82,6 +82,11 @@
|
||||
3 Color Job… Задание с 3 цветами…
|
||||
30° Angles Углы 30°
|
||||
3D and Materials 3D и материалы
|
||||
Plane Плоскость
|
||||
Extrude Выдавливание
|
||||
Revolve Вращение
|
||||
Inflate Надувание
|
||||
Select objects, then pick an object type to give them a 3D effect. Выделите объекты, затем выберите тип объекта, чтобы придать им 3D-эффект.
|
||||
3D Ribbon 3D-лента
|
||||
45° Angles Углы 45°
|
||||
60° Angles Углы 60°
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -72,6 +72,11 @@
|
||||
3 Color Job… 3-färgsjobb…
|
||||
30° Angles 30°-vinklar
|
||||
3D and Materials 3D och material
|
||||
Plane Plan
|
||||
Extrude Extrudera
|
||||
Revolve Rotera runt axel
|
||||
Inflate Blås upp
|
||||
Select objects, then pick an object type to give them a 3D effect. Markera objekt och välj sedan en objekttyp för att ge dem en 3D-effekt.
|
||||
3D Ribbon 3D-band
|
||||
45° Angles 45°-vinklar
|
||||
60° Angles 60°-vinklar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -69,6 +69,11 @@
|
||||
3 Color Job… 3 Renkli İş…
|
||||
30° Angles 30° Açılar
|
||||
3D and Materials 3D ve Malzemeler
|
||||
Plane Düzlem
|
||||
Extrude Ekstrüzyon
|
||||
Revolve Döndür
|
||||
Inflate Şişir
|
||||
Select objects, then pick an object type to give them a 3D effect. Nesneleri seçin, ardından onlara 3D efekti vermek için bir nesne türü seçin.
|
||||
3D Ribbon 3D Şerit
|
||||
45° Angles 45° Açılar
|
||||
60° Angles 60° Açılar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -78,6 +78,11 @@
|
||||
3 Color Job… Завдання з 3 кольорами…
|
||||
30° Angles Кути 30°
|
||||
3D and Materials 3D та матеріали
|
||||
Plane Площина
|
||||
Extrude Видавлювання
|
||||
Revolve Обертання
|
||||
Inflate Надування
|
||||
Select objects, then pick an object type to give them a 3D effect. Виділіть об’єкти, потім виберіть тип об’єкта, щоб надати їм 3D-ефект.
|
||||
3D Ribbon 3D-стрічка
|
||||
45° Angles Кути 45°
|
||||
60° Angles Кути 60°
|
||||
|
||||
|
@@ -83,6 +83,11 @@
|
||||
3 x 3 Average Trung bình 3 x 3
|
||||
30° Angles Góc 30°
|
||||
3D and Materials 3D và vật liệu
|
||||
Plane Mặt phẳng
|
||||
Extrude Kéo dài
|
||||
Revolve Xoay tròn
|
||||
Inflate Phình
|
||||
Select objects, then pick an object type to give them a 3D effect. Chọn đối tượng, rồi chọn một kiểu đối tượng để tạo hiệu ứng 3D cho chúng.
|
||||
3D Ribbon Dải 3D
|
||||
45° Angles Góc 45°
|
||||
5 x 5 Average Trung bình 5 x 5
|
||||
|
||||
|
@@ -26,6 +26,11 @@
|
||||
3 Color Job… 3 色作业…
|
||||
30° Angles 30° 角
|
||||
3D and Materials 3D 与材质
|
||||
Plane 平面
|
||||
Extrude 突出
|
||||
Revolve 回转
|
||||
Inflate 膨胀
|
||||
Select objects, then pick an object type to give them a 3D effect. 选择对象,然后选取一种对象类型,为其添加 3D 效果。
|
||||
3D Ribbon 3D 缎带
|
||||
45° Angles 45° 角
|
||||
60° Angles 60° 角
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -34,6 +34,11 @@
|
||||
3 Color Job… 3 色印刷作業…
|
||||
30° Angles 30° 角度
|
||||
3D and Materials 3D 與材質
|
||||
Plane 平面
|
||||
Extrude 突出
|
||||
Revolve 迴轉
|
||||
Inflate 膨脹
|
||||
Select objects, then pick an object type to give them a 3D effect. 選取物件,然後挑選一種物件類型,為其加上 3D 效果。
|
||||
3D Ribbon 3D 緞帶
|
||||
45° Angles 45° 角度
|
||||
60° Angles 60° 角度
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -629,6 +629,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
];
|
||||
let lang = crate::i18n::current();
|
||||
let mut panels = vec![
|
||||
panel("3D and Materials", crate::panels::three_d::ID),
|
||||
panel("Actions", "actions"),
|
||||
panel("Align", "align"),
|
||||
panel("Appearance", "appearance"),
|
||||
|
||||
@@ -43,6 +43,7 @@ pub mod swatches;
|
||||
pub mod symbols;
|
||||
pub mod tabs;
|
||||
pub mod text_styles;
|
||||
pub mod three_d;
|
||||
pub mod transform;
|
||||
pub mod transparency;
|
||||
pub mod variables;
|
||||
@@ -179,6 +180,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
links::ID => links::show(app, ui),
|
||||
asset_export::ID => asset_export::show(app, ui),
|
||||
css_properties::ID => css_properties::show(app, ui),
|
||||
three_d::ID => three_d::show(app, ui),
|
||||
crate::addons::cleanup::ID => crate::addons::cleanup::show(app, ui),
|
||||
crate::addons::color::ID => crate::addons::color::show(app, ui),
|
||||
crate::addons::pathtools::ID => crate::addons::pathtools::show(app, ui),
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
//! The 3D and Materials panel drawn headless: its Window menu entry, the object types applying or
|
||||
//! changing a 3D effect, the dialogs' controls for the applied effect, and one undo step for a
|
||||
//! drag or a typed value.
|
||||
#![allow(clippy::unwrap_used, clippy::panic)]
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::{ID, Target, changed_settings, send, set_type, show, target};
|
||||
use crate::tests_labels::painted_text;
|
||||
use crate::{Services, VectorcraftApp};
|
||||
|
||||
/// An app with a selected blue square.
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
|
||||
app.run("paint.setFill", json!({"color": "#338ad6"})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
fn undo_len(app: &VectorcraftApp) -> usize {
|
||||
app.session.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
fn effect(app: &VectorcraftApp) -> (String, Value) {
|
||||
let t = target(app).unwrap();
|
||||
(t.effect, t.params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_menu_opens_the_panel() {
|
||||
let mut app = app();
|
||||
assert!(crate::state::ICON_PANELS.iter().any(|(id, label, _)| *id == ID && *label == "3D and Materials"));
|
||||
app.run("window.panel", json!({"panel": ID})).unwrap();
|
||||
assert!(crate::panel_docking::showing(&app.ui, ID));
|
||||
app.run("window.panel", json!({"panel": "3D and Materials"})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_3d_effect_the_panel_offers_the_object_types() {
|
||||
let mut app = app();
|
||||
let text = painted_text(&mut app, show);
|
||||
for label in ["Plane", "Extrude", "Revolve", "Inflate", "Select objects, then pick an object type"] {
|
||||
assert!(text.contains(label), "{label} in {text}");
|
||||
}
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
let text = painted_text(&mut app, show);
|
||||
assert!(text.contains("Select objects, then pick an object type"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_type_applies_its_effect_then_changes_it_in_place() {
|
||||
let mut app = app();
|
||||
let history = undo_len(&app);
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert_eq!(effect(&app).0, "threeD.extrude");
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
app.run("effect.setParams", json!({"index": 0, "item": null, "params": {"rotationX": 15.0, "depth": 70.0}})).unwrap();
|
||||
for (kind, id) in [("inflate", "threeD.inflate"), ("revolve", "threeD.revolve"), ("plane", "threeD.rotate"), ("extrude", "threeD.extrude")] {
|
||||
let history = undo_len(&app);
|
||||
set_type(&mut app, kind).unwrap();
|
||||
let (now, params) = effect(&app);
|
||||
assert_eq!(now, id);
|
||||
assert_eq!(params["rotationX"], 15.0, "{kind}");
|
||||
assert_eq!(undo_len(&app), history + 1, "{kind}");
|
||||
// The object keeps one effect: the type changed in place.
|
||||
assert_eq!(app.session.active().unwrap().doc.node(app.session.active().unwrap().selection.objects[0]).unwrap().appearance.effects.len(), 1);
|
||||
}
|
||||
// The type it already has runs nothing.
|
||||
let history = undo_len(&app);
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert_eq!(undo_len(&app), history);
|
||||
assert!(set_type(&mut app, "cube").unwrap_err().contains("must be one of plane, extrude, revolve, inflate"));
|
||||
assert_eq!(undo_len(&app), history);
|
||||
}
|
||||
|
||||
/// The ids of the effects of objects `ids`, in order.
|
||||
fn effect_ids(app: &VectorcraftApp, ids: &[u64]) -> Vec<Vec<String>> {
|
||||
let st = app.session.active().unwrap();
|
||||
ids.iter().map(|id| st.doc.node(vectorcraft_doc::NodeId(*id)).unwrap().appearance.effects.iter().map(|e| e.id.clone()).collect()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_object_type_reaches_every_selected_object_in_one_undo_step() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.inflate", "item": null})).unwrap();
|
||||
let c = app.run("shape.rectangle", json!({"x": 100, "y": 250, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
// The first object already has the type; the second has another; the third has none.
|
||||
for order in [[a, b, c], [c, b, a]] {
|
||||
app.run("select.set", json!({"ids": order})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let history = undo_len(&app);
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert_eq!(effect_ids(&app, &[a, b, c]), vec![vec!["threeD.extrude".to_string()]; 3], "{order:?}");
|
||||
assert_eq!(undo_len(&app), history + 1, "{order:?}");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "{order:?}");
|
||||
}
|
||||
// Every selected object already has the type: nothing is recorded.
|
||||
app.run("select.set", json!({"ids": [a, b, c]})).unwrap();
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let history = undo_len(&app);
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert_eq!(undo_len(&app), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_ends_when_the_selection_goes_away() {
|
||||
let mut app = app();
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let history = undo_len(&app);
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(90.0))]), true);
|
||||
assert!(app.session.in_interaction());
|
||||
app.session.active_mut().unwrap().selection.objects.clear();
|
||||
ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui)).textures_delta.clear();
|
||||
assert!(!app.session.in_interaction());
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_nothing_selected_an_object_type_changes_nothing() {
|
||||
let mut app = app();
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
let history = undo_len(&app);
|
||||
assert!(set_type(&mut app, "plane").is_err());
|
||||
assert_eq!(undo_len(&app), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_shows_the_dialog_controls_of_each_type() {
|
||||
let mut app = app();
|
||||
for (kind, label) in [("extrude", "Depth"), ("inflate", "Volume"), ("revolve", "Revolve angle"), ("plane", "Rotation X")] {
|
||||
set_type(&mut app, kind).unwrap();
|
||||
let text = painted_text(&mut app, show);
|
||||
assert!(text.contains(label), "{kind}: {label} in {text}");
|
||||
assert!(text.contains("Lighting"), "{kind}: {text}");
|
||||
// Drawing the panel is no edit.
|
||||
let history = undo_len(&app);
|
||||
painted_text(&mut app, show);
|
||||
assert_eq!(undo_len(&app), history, "{kind}");
|
||||
}
|
||||
// The dialog's artboard-axis toggle has no artboard gizmo in the panel.
|
||||
set_type(&mut app, "revolve").unwrap();
|
||||
assert!(!painted_text(&mut app, show).contains("Show axis"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_previews_and_records_one_undo_step_when_released() {
|
||||
let mut app = app();
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let history = undo_len(&app);
|
||||
for depth in [60.0, 80.0, 120.0] {
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(depth))]), true);
|
||||
assert_eq!(effect(&app).1["depth"], depth);
|
||||
}
|
||||
// Another setting in the same drag keeps the earlier one.
|
||||
send(&mut app, &ctx, Map::from_iter([("twist".to_string(), json!(45.0))]), true);
|
||||
assert_eq!(effect(&app).1["depth"], 120.0);
|
||||
assert_eq!(undo_len(&app), history);
|
||||
// Released without a change in the last frame: the drag commits.
|
||||
send(&mut app, &ctx, Map::new(), false);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
let (_, params) = effect(&app);
|
||||
assert_eq!((params["depth"].clone(), params["twist"].clone()), (json!(120.0), json!(45.0)));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(effect(&app).1["depth"], 50.0);
|
||||
// A typed value is one undo step at once.
|
||||
send(&mut app, &ctx, Map::from_iter([("taper".to_string(), json!(60.0))]), false);
|
||||
assert_eq!(effect(&app).1["taper"], 60.0);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
// Nothing changed and no drag: nothing runs.
|
||||
send(&mut app, &ctx, Map::new(), false);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_reaches_each_selected_3d_effect_and_no_other_effect() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
// A second object with Inflate after a drop shadow, a third with only a drop shadow.
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "stylize.dropShadow", "item": null})).unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.inflate", "item": null})).unwrap();
|
||||
let c = app.run("shape.rectangle", json!({"x": 100, "y": 250, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "stylize.dropShadow", "item": null})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b, c]})).unwrap();
|
||||
let shadow = |app: &VectorcraftApp, id: u64| {
|
||||
app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params.clone()
|
||||
};
|
||||
let (b_shadow, c_shadow) = (shadow(&app, b), shadow(&app, c));
|
||||
let history = undo_len(&app);
|
||||
let ctx = egui::Context::default();
|
||||
// `bevel` is an Extrude & Bevel setting only; `depth` both effects take.
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(75.0)), ("bevel".to_string(), json!("classic"))]), true);
|
||||
send(&mut app, &ctx, Map::new(), false);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
let st = app.session.active().unwrap();
|
||||
let fx = |id: u64| st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects.clone();
|
||||
assert_eq!(fx(a)[0].params["depth"], 75.0);
|
||||
assert_eq!(fx(a)[0].params["bevel"], "classic");
|
||||
assert_eq!(fx(b)[1].params["depth"], 75.0);
|
||||
assert!(fx(b)[1].params.get("bevel").is_none());
|
||||
assert_eq!(fx(b)[0].params, b_shadow);
|
||||
assert_eq!(fx(c)[0].params, c_shadow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_finds_a_3d_effect_on_a_fill() {
|
||||
let mut app = app();
|
||||
app.run("effect.apply", json!({"effect": "threeD.inflate", "item": 0})).unwrap();
|
||||
let t = target(&app).unwrap();
|
||||
assert_eq!((t.item, t.index, t.effect.as_str()), (Some(0), 0, "threeD.inflate"));
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
assert_eq!(target(&app).map(|t: Target| (t.item, t.effect)), Some((Some(0), "threeD.extrude".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_the_selected_effects_disagree_on_are_unset_so_a_reset_reaches_them_all() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"depth": 80.0}})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let mut fields = target(&app).map(|t| vectorcraft_effects::merged_params(&t.effect, &t.params)).unwrap().as_object().cloned().unwrap();
|
||||
super::mark_mixed(&app, &mut fields);
|
||||
assert_eq!(fields["depth"], Value::Null);
|
||||
assert_eq!(fields["twist"], 0.0);
|
||||
// Resetting Depth to its default (the first object's value) is a change for both objects.
|
||||
let before = Value::Object(fields.clone());
|
||||
fields.insert("depth".into(), json!(50.0));
|
||||
let changed = changed_settings(&before, &Value::Object(fields));
|
||||
assert_eq!(Value::Object(changed.clone()), json!({"depth": 50.0}));
|
||||
send(&mut app, &egui::Context::default(), changed, false);
|
||||
let st = app.session.active().unwrap();
|
||||
for id in [a, b] {
|
||||
assert_eq!(st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params["depth"], 50.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_light_list_edit_reaches_only_the_objects_with_the_same_lights() {
|
||||
let mut app = app();
|
||||
let two = json!([{"color": "#ff8040"}, {"color": "#4080ff", "rotation": -35.0}]);
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"lights": two}})).unwrap();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
let add = |app: &mut VectorcraftApp, x: f64, lights: Value| {
|
||||
let id = app.run("shape.rectangle", json!({"x": x, "y": 250, "width": 80, "height": 80})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"lights": lights}})).unwrap();
|
||||
id
|
||||
};
|
||||
let b = add(&mut app, 50.0, json!([{"color": "#00ff00"}]));
|
||||
let c = add(&mut app, 200.0, two.clone());
|
||||
app.run("select.set", json!({"ids": [a, b, c]})).unwrap();
|
||||
let lights = |app: &VectorcraftApp, id: u64| {
|
||||
app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params["lights"].clone()
|
||||
};
|
||||
let b_before = lights(&app, b);
|
||||
let three = json!([{"color": "#ff8040"}, {"color": "#4080ff", "rotation": -35.0}, {"color": "#ffffff", "rotation": 0.0, "height": 90.0}]);
|
||||
let history = undo_len(&app);
|
||||
send(&mut app, &egui::Context::default(), Map::from_iter([("lights".to_string(), three.clone())]), false);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
assert_eq!(lights(&app, a).as_array().map(Vec::len), Some(3));
|
||||
assert_eq!(lights(&app, c), lights(&app, a));
|
||||
assert_eq!(lights(&app, b), b_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_keeps_its_objects_and_a_new_selection_starts_a_new_step() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0];
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = vectorcraft_doc::NodeId(b);
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a.0]})).unwrap();
|
||||
let history = undo_len(&app);
|
||||
let ctx = egui::Context::default();
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(90.0))]), true);
|
||||
// The selection changes during the drag: the drag on `a` is committed, and `b` starts its own.
|
||||
app.session.active_mut().unwrap().selection.objects = vec![b];
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(70.0))]), true);
|
||||
send(&mut app, &ctx, Map::new(), false);
|
||||
let depth = |app: &VectorcraftApp, id| app.session.active().unwrap().doc.node(id).unwrap().appearance.effects[0].params["depth"].clone();
|
||||
assert_eq!(depth(&app, a), 90.0);
|
||||
assert_eq!(depth(&app, b), 70.0);
|
||||
assert_eq!(undo_len(&app), history + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_light_drag_keeps_the_objects_whose_lights_matched_when_it_started() {
|
||||
let mut app = app();
|
||||
let one = |color: &str| json!([{"color": color}]);
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"lights": one("#ff0000")}})).unwrap();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"lights": one("#00ff00")}})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
// The first object's list passes through the second's value during the drag.
|
||||
send(&mut app, &ctx, Map::from_iter([("lights".to_string(), one("#00ff00"))]), true);
|
||||
send(&mut app, &ctx, Map::from_iter([("lights".to_string(), one("#0000ff"))]), true);
|
||||
send(&mut app, &ctx, Map::new(), false);
|
||||
let lights =
|
||||
|id: u64| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params["lights"].clone();
|
||||
assert_eq!(lights(a), one("#0000ff"));
|
||||
assert_eq!(lights(b), one("#00ff00"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_ends_when_the_panel_is_no_longer_drawn() {
|
||||
let mut app = app();
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let history = undo_len(&app);
|
||||
ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui)).textures_delta.clear();
|
||||
send(&mut app, &ctx, Map::from_iter([("depth".to_string(), json!(90.0))]), true);
|
||||
// A pass that draws the panel keeps the drag; the first pass that does not draw it ends it.
|
||||
ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
// Drawn with the pointer still down: only the mark `show` leaves.
|
||||
crate::panels::set_pstate(ui.ctx(), super::DRAWN, Some(super::root_pass(ui.ctx())));
|
||||
super::end_stale_drag(&mut app, ui.ctx());
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
assert!(app.session.in_interaction());
|
||||
ctx.run_ui(egui::RawInput::default(), |ui| super::end_stale_drag(&mut app, ui.ctx())).textures_delta.clear();
|
||||
assert!(!app.session.in_interaction());
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
assert_eq!(effect(&app).1["depth"], 90.0);
|
||||
}
|
||||
|
||||
/// The panel's controls, drawn once with the accessibility tree on.
|
||||
fn controls(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<egui::accesskit::Node> {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn named<'a>(nodes: &'a [egui::accesskit::Node], label: &str) -> &'a egui::accesskit::Node {
|
||||
nodes.iter().find(|n| n.label() == Some(label)).unwrap_or_else(|| panic!("no {label:?}"))
|
||||
}
|
||||
|
||||
/// A checkbox setting the selected effects hold with different values shows a dash, leaves the
|
||||
/// controls it guards available, and another change leaves it as each effect has it.
|
||||
#[test]
|
||||
fn a_mixed_checkbox_shows_a_dash_and_another_change_keeps_each_effects_value() {
|
||||
use crate::panels::tests_appearance::{click, frame_events, text_rect};
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "extrude").unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.extrude", "item": null, "params": {"shade": false}})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
ctx.enable_accesskit();
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
click(&ctx, &mut app, text_rect(&texts, "Lighting").center(), show);
|
||||
let nodes = controls(&mut app, &ctx);
|
||||
assert_eq!(named(&nodes, "Shading").toggled(), Some(egui::accesskit::Toggled::Mixed), "a mixed Shading shows a dash");
|
||||
let behind = named(&nodes, "Light behind the object");
|
||||
assert!(!behind.is_disabled(), "a mixed Shading leaves the light settings available");
|
||||
assert!(!named(&nodes, "Add Light").is_disabled());
|
||||
let at = behind.bounds().map(|r| egui::pos2(((r.x0 + r.x1) / 2.0) as f32, ((r.y0 + r.y1) / 2.0) as f32)).unwrap();
|
||||
let history = undo_len(&app);
|
||||
click(&ctx, &mut app, at, show);
|
||||
assert_eq!(undo_len(&app), history + 1);
|
||||
let params = |id: u64| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].params.clone();
|
||||
let merged = |id: u64| vectorcraft_effects::merged_params("threeD.extrude", ¶ms(id));
|
||||
assert_eq!((merged(a)["shade"].clone(), merged(b)["shade"].clone()), (json!(true), json!(false)));
|
||||
assert_eq!((merged(a)["lightBehind"].clone(), merged(b)["lightBehind"].clone()), (json!(true), json!(true)));
|
||||
// A single light whose settings differ: Add Light is off.
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("effect.setParams", json!({"index": 0, "item": null, "params": {"lightIntensity": 40.0}})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
assert!(named(&controls(&mut app, &ctx), "Add Light").is_disabled());
|
||||
}
|
||||
|
||||
/// Revolve's Cap checkbox shows a dash when the selected effects disagree on it.
|
||||
#[test]
|
||||
fn a_mixed_revolve_cap_shows_a_dash() {
|
||||
let mut app = app();
|
||||
let a = app.session.active().unwrap().selection.objects[0].0;
|
||||
set_type(&mut app, "revolve").unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 250, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("effect.apply", json!({"effect": "threeD.revolve", "item": null, "params": {"cap": false}})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
ctx.enable_accesskit();
|
||||
let history = undo_len(&app);
|
||||
assert_eq!(named(&controls(&mut app, &ctx), "Cap").toggled(), Some(egui::accesskit::Toggled::Mixed));
|
||||
assert_eq!(undo_len(&app), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_settings_lists_only_new_values() {
|
||||
let before = json!({"depth": 50.0, "cap": true, "bevel": "none"});
|
||||
let after = json!({"depth": 60.0, "cap": true, "bevel": "classic", "twist": 0.0});
|
||||
let changed = changed_settings(&before, &after);
|
||||
assert_eq!(Value::Object(changed), json!({"depth": 60.0, "bevel": "classic", "twist": 0.0}));
|
||||
assert!(changed_settings(&after, &after).is_empty());
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
//! Window › 3D and Materials: edits the selection's 3D effect in place. The object types (Plane,
|
||||
//! Extrude, Revolve, Inflate) apply a 3D effect to a selection without one (`effect.apply`) or
|
||||
//! change the applied one (`effect.setParams` with `threeDType`). Below them are the 3D effect
|
||||
//! dialogs' own Object, Rotation and Lighting controls; each change runs `effect.setParams` as one
|
||||
//! undo step, and a drag previews and commits once, when the pointer is released.
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::{Effect, Node};
|
||||
use vectorcraft_effects::{THREE_D_TYPES, is_three_d};
|
||||
|
||||
use super::{live_run, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::state::Dialog;
|
||||
use crate::widgets::{self, Live, dim_label};
|
||||
|
||||
/// The panel's id (`window.panel`).
|
||||
pub const ID: &str = "threeD";
|
||||
|
||||
/// The name of the undo step a change makes, as the effect dialogs' commits name it.
|
||||
const LABEL: &str = "Effect Options";
|
||||
|
||||
/// The selection's 3D effect: the appearance item whose effects hold it (`None`: the object's
|
||||
/// own effects), its index there, and its id and parameters.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Target {
|
||||
pub item: Option<usize>,
|
||||
pub index: usize,
|
||||
pub effect: String,
|
||||
pub params: Value,
|
||||
}
|
||||
|
||||
/// The 3D effect of the first selected object: its own effects first, then its fills' and
|
||||
/// strokes'. `None` without a selection or without a 3D effect.
|
||||
pub(crate) fn target(app: &VectorcraftApp) -> Option<Target> {
|
||||
let st = app.session.active()?;
|
||||
let n = st.selection.objects.first().and_then(|id| st.doc.node(*id))?;
|
||||
find(n)
|
||||
}
|
||||
|
||||
fn find(n: &Node) -> Option<Target> {
|
||||
let in_list = |fx: &[Effect], item: Option<usize>| {
|
||||
fx.iter().position(|e| is_three_d(&e.id)).map(|index| Target { item, index, effect: fx[index].id.clone(), params: fx[index].params.clone() })
|
||||
};
|
||||
in_list(&n.appearance.effects, None).or_else(|| n.appearance.items.iter().enumerate().find_map(|(k, item)| in_list(item.effects(), Some(k))))
|
||||
}
|
||||
|
||||
/// The panel's object type of effect `id` (`plane`, `extrude`, `revolve` or `inflate`).
|
||||
pub(crate) fn type_of(id: &str) -> Option<&'static str> {
|
||||
THREE_D_TYPES.iter().find(|(_, effect)| *effect == id).map(|(kind, _)| *kind)
|
||||
}
|
||||
|
||||
/// Picks object type `kind` for each selected object: changes its 3D effect into that type, or
|
||||
/// applies that type's effect to it when it has none, all as one undo step. An object whose 3D
|
||||
/// effect already has that type is left as it is (`effect.setParams` records nothing for it).
|
||||
pub(crate) fn set_type(app: &mut VectorcraftApp, kind: &str) -> Result<(), String> {
|
||||
let Some(&(_, effect)) = THREE_D_TYPES.iter().find(|(k, _)| *k == kind) else {
|
||||
let key = "threeDType";
|
||||
return Err(format!("`{key}` must be one of {}", THREE_D_TYPES.map(|(k, _)| k).join(", ")));
|
||||
};
|
||||
let steps: Vec<(&str, Value)> = app.session.active().map_or_else(Vec::new, |st| {
|
||||
st.selection
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|id| st.doc.node(*id).map(|n| (id.0, find(n))))
|
||||
.map(|(id, t)| match t {
|
||||
Some(t) => ("effect.setParams", json!({"ids": [id], "index": t.index, "item": t.item, "threeDType": kind})),
|
||||
None => ("effect.apply", json!({"ids": [id], "effect": effect, "item": null})),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
match steps.as_slice() {
|
||||
// Without a selection, `effect.apply` says why nothing changed.
|
||||
[] => app.run("effect.apply", json!({"effect": effect, "item": null})).map(|_| ()),
|
||||
[(id, p)] => app.run(id, p.clone()).map(|_| ()),
|
||||
_ => {
|
||||
app.run("edit.undoGroup.begin", json!({"label": LABEL}))?;
|
||||
for (id, p) in &steps {
|
||||
if let Err(e) = app.run(id, p.clone()) {
|
||||
app.run("edit.undoGroup.end", json!({"cancel": true})).ok();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
app.run("edit.undoGroup.end", json!({})).map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commits the drag in progress, when the panel has one.
|
||||
fn finish(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if !pstate::<Map<String, Value>>(ctx, PENDING).is_empty() {
|
||||
app.session.commit_interaction().ok();
|
||||
app.sync_views();
|
||||
}
|
||||
set_pstate(ctx, PENDING, Map::<String, Value>::new());
|
||||
set_pstate(ctx, PLAN, None::<Plan>);
|
||||
}
|
||||
|
||||
/// What a change goes to, taken when it starts (before any preview): the selected objects' ids,
|
||||
/// and each selected object with a 3D effect, the effect, and whether its list of lights matches
|
||||
/// the first one's (a light list edited in the panel replaces only those).
|
||||
#[derive(Clone, Debug)]
|
||||
struct Plan {
|
||||
selection: Vec<u64>,
|
||||
targets: Vec<(u64, Target, bool)>,
|
||||
}
|
||||
|
||||
fn plan(app: &VectorcraftApp) -> Plan {
|
||||
let Some(st) = app.session.active() else { return Plan { selection: vec![], targets: vec![] } };
|
||||
let selection: Vec<u64> = st.selection.objects.iter().map(|id| id.0).collect();
|
||||
let found: Vec<(u64, Target)> = st.selection.objects.iter().filter_map(|id| Some((id.0, find(st.doc.node(*id)?)?))).collect();
|
||||
let lights = |t: &Target| vectorcraft_effects::merged_params(&t.effect, &t.params).get("lights").cloned();
|
||||
let first = found.first().map(|(_, t)| lights(t));
|
||||
let targets = found
|
||||
.into_iter()
|
||||
.map(|(id, t)| {
|
||||
let same = first.as_ref() == Some(&lights(&t));
|
||||
(id, t, same)
|
||||
})
|
||||
.collect();
|
||||
Plan { selection, targets }
|
||||
}
|
||||
|
||||
/// Sends the settings `changed` to the 3D effect of each selected object (only the settings its
|
||||
/// effect takes; objects without a 3D effect are left as they are): previews them while the
|
||||
/// pointer is down (`dragging`) and commits them as one undo step when it is released. Settings
|
||||
/// changed earlier in the same drag are sent again with them, to the objects the drag started
|
||||
/// with, since each preview starts again from the document as it was before the drag. A drag
|
||||
/// whose selection changes is committed first, and the change starts a new one.
|
||||
pub(crate) fn send(app: &mut VectorcraftApp, ctx: &egui::Context, changed: Map<String, Value>, dragging: bool) {
|
||||
if changed.is_empty() {
|
||||
// A drag that ended without a change in its last frame is committed now.
|
||||
if !dragging {
|
||||
finish(app, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let selection: Vec<u64> = app.session.active().map(|st| st.selection.objects.iter().map(|id| id.0).collect()).unwrap_or_default();
|
||||
let mut held: Option<Plan> = pstate(ctx, PLAN);
|
||||
if held.as_ref().is_some_and(|p| p.selection != selection) {
|
||||
finish(app, ctx);
|
||||
held = None;
|
||||
}
|
||||
let plan = held.unwrap_or_else(|| plan(app));
|
||||
let mut pending: Map<String, Value> = pstate(ctx, PENDING);
|
||||
pending.extend(changed);
|
||||
let steps: Vec<Value> = plan
|
||||
.targets
|
||||
.iter()
|
||||
.filter_map(|(id, t, same_lights)| {
|
||||
let takes = vectorcraft_effects::default_params(&t.effect).unwrap_or(Value::Null);
|
||||
let params: Map<String, Value> = pending
|
||||
.iter()
|
||||
.filter(|(k, _)| takes.get(k.as_str()).is_some() && (k.as_str() != "lights" || *same_lights))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
(!params.is_empty())
|
||||
.then(|| json!({"command": "effect.setParams", "params": {"ids": [id], "index": t.index, "item": t.item, "params": params}}))
|
||||
})
|
||||
.collect();
|
||||
let phase = if dragging { Live::Dragging } else { Live::Released };
|
||||
match steps.as_slice() {
|
||||
[] => {}
|
||||
[step] => live_run(app, LABEL, "effect.setParams", step["params"].clone(), phase),
|
||||
_ => live_run(app, LABEL, "command.batch", json!({"label": LABEL, "commands": steps}), phase),
|
||||
}
|
||||
if dragging {
|
||||
set_pstate(ctx, PENDING, pending);
|
||||
set_pstate(ctx, PLAN, Some(plan));
|
||||
} else {
|
||||
set_pstate(ctx, PENDING, Map::<String, Value>::new());
|
||||
set_pstate(ctx, PLAN, None::<Plan>);
|
||||
}
|
||||
}
|
||||
|
||||
/// Panel state key of the [`Plan`] of the drag in progress.
|
||||
const PLAN: &str = "three-d-plan";
|
||||
/// Panel state key of the pass the panel was last drawn in.
|
||||
const DRAWN: &str = "three-d-drawn";
|
||||
|
||||
/// Commits the panel's drag in progress when the panel was not drawn in this pass (it was closed
|
||||
/// or hidden during the drag), so the drag is one undo step and no preview stays open into a later
|
||||
/// pass. The app runs this at the end of every pass, after the panels.
|
||||
pub(crate) fn end_stale_drag(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let drawn: Option<u64> = pstate(ctx, DRAWN);
|
||||
if drawn.is_some_and(|pass| pass != root_pass(ctx)) {
|
||||
finish(app, ctx);
|
||||
set_pstate(ctx, DRAWN, None::<u64>);
|
||||
}
|
||||
}
|
||||
|
||||
/// The main window's pass in progress. A floating window draws inside it and counts its own
|
||||
/// passes, so the panel's mark uses this one wherever the panel draws.
|
||||
fn root_pass(ctx: &egui::Context) -> u64 {
|
||||
ctx.cumulative_pass_nr_for(egui::ViewportId::ROOT)
|
||||
}
|
||||
|
||||
/// Panel state key of the settings changed during the drag in progress.
|
||||
const PENDING: &str = "three-d-pending";
|
||||
/// Panel state key of the controls' own state (the open tab, the selected light).
|
||||
const UI_STATE: &str = "three-d-ui";
|
||||
|
||||
/// The 3D and Materials panel.
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
set_pstate(&ctx, DRAWN, Some(root_pass(&ctx)));
|
||||
let target = target(app);
|
||||
let selected = app.session.active().is_some_and(|st| !st.selection.objects.is_empty());
|
||||
let current = target.as_ref().and_then(|t| type_of(&t.effect));
|
||||
let labels = [tl!("Plane"), tl!("Extrude"), tl!("Revolve"), tl!("Inflate")];
|
||||
let at = current.and_then(|kind| THREE_D_TYPES.iter().position(|(k, _)| *k == kind)).unwrap_or(usize::MAX);
|
||||
let picked = ui.add_enabled_ui(selected, |ui| widgets::tab_bar(ui, &labels, at)).inner;
|
||||
if let Some((kind, _)) = picked.and_then(|i| THREE_D_TYPES.get(i))
|
||||
&& let Err(e) = set_type(app, kind)
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
let Some(t) = target else {
|
||||
// The selection changed during a drag: the drag ends with what it previewed.
|
||||
finish(app, &ctx);
|
||||
dim_label(ui, tl!("Select objects, then pick an object type to give them a 3D effect."));
|
||||
return;
|
||||
};
|
||||
// The applied effect's settings in the dialog's form, with the controls' own state.
|
||||
let mut fields = vectorcraft_effects::merged_params(&t.effect, &t.params).as_object().cloned().unwrap_or_default();
|
||||
mark_mixed(app, &mut fields);
|
||||
fields.extend(pstate::<Map<String, Value>>(&ctx, UI_STATE));
|
||||
fields.insert("__effect".into(), json!(t.effect));
|
||||
fields.insert("__panel".into(), json!(true));
|
||||
let mut d = Dialog { kind: ID.into(), fields };
|
||||
let before = crate::dialogs::form::params(&d);
|
||||
crate::dialogs::revolve::controls(app, ui, &mut d);
|
||||
let state: Map<String, Value> = d
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("__") && !matches!(k.as_str(), "__effect" | "__panel"))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
set_pstate(&ctx, UI_STATE, state);
|
||||
let after = crate::dialogs::form::params(&d);
|
||||
let changed = changed_settings(&before, &after);
|
||||
let dragging = ctx.input(|i| i.pointer.any_down());
|
||||
send(app, &ctx, changed, dragging);
|
||||
}
|
||||
|
||||
/// Sets to null each setting in `fields` (the first selected object's 3D effect) that the 3D effect
|
||||
/// of another selected object has with another value. The controls then show it at its default
|
||||
/// (a checkbox shows a dash) with its reset dot filled, and its reset dot, a menu choice, a checkbox click, or a number other
|
||||
/// than the default shown is a change sent to every effect. The light list (`lights`, where null
|
||||
/// is the single light) is left as it is.
|
||||
pub(crate) fn mark_mixed(app: &VectorcraftApp, fields: &mut Map<String, Value>) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
for other in st.selection.objects.iter().skip(1).filter_map(|id| find(st.doc.node(*id)?)) {
|
||||
let other = vectorcraft_effects::merged_params(&other.effect, &other.params);
|
||||
for (key, value) in fields.iter_mut().filter(|(k, _)| k.as_str() != "lights") {
|
||||
if other.get(key.as_str()).is_some_and(|v| v != value) {
|
||||
*value = Value::Null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings whose values differ between `before` and `after`, with their values in `after`.
|
||||
pub(crate) fn changed_settings(before: &Value, after: &Value) -> Map<String, Value> {
|
||||
let Some(after) = after.as_object() else { return Map::new() };
|
||||
after.iter().filter(|(k, v)| before.get(k.as_str()) != Some(*v)).map(|(k, v)| (k.clone(), v.clone())).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests_three_d.rs"]
|
||||
mod tests;
|
||||
@@ -204,6 +204,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
(crate::panels::links::ID, "Links", "link"),
|
||||
(crate::panels::asset_export::ID, "Asset Export", "share-2"),
|
||||
(crate::panels::css_properties::ID, "CSS Properties", "globe"),
|
||||
(crate::panels::three_d::ID, "3D and Materials", "dc-cube"),
|
||||
(crate::panels::variables::ID, "Variables", "chart-column"),
|
||||
(crate::addons::cleanup::ID, "Artwork Cleanup", "sparkles"),
|
||||
(crate::addons::color::ID, "Color Effects", "sun"),
|
||||
|
||||
@@ -70,7 +70,7 @@ pub use text::{
|
||||
exact_text_field, exact_text_field_named, plain_field, plain_field_named, search_field, text_field, text_field_named, text_presets,
|
||||
text_presets_named,
|
||||
};
|
||||
pub use toggles::{check, check_box, check_name, check_tip, check3, radio, radio_button};
|
||||
pub use toggles::{check, check_box, check_name, check_tip, check3, check3_tip, radio, radio_button};
|
||||
|
||||
/// A widget id salt that needs only [`std::hash::Hash`]. egui 0.37's [`egui::AsIdSalt`] also
|
||||
/// requires `Debug` for its debug-only id-source registry; VectorCraft's public field helpers do
|
||||
|
||||
@@ -26,7 +26,12 @@ pub fn check(ui: &mut Ui, label: &str, value: bool, enabled: bool) -> bool {
|
||||
|
||||
/// [`check`] with tooltip `tip` on the box and its label. Returns true when toggled.
|
||||
pub fn check_tip(ui: &mut Ui, label: &str, value: bool, enabled: bool, tip: &str) -> bool {
|
||||
let (clicked, resp) = check_row(ui, tl!(label), Some(value), enabled);
|
||||
check3_tip(ui, label, Some(value), enabled, tip)
|
||||
}
|
||||
|
||||
/// [`check3`] with tooltip `tip` on the box and its label. Returns true when clicked.
|
||||
pub fn check3_tip(ui: &mut Ui, label: &str, value: Option<bool>, enabled: bool, tip: &str) -> bool {
|
||||
let (clicked, resp) = check_row(ui, tl!(label), value, enabled);
|
||||
resp.on_hover_text(tip);
|
||||
clicked
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ VectorCraft choices; an original approximation, never compared with another appl
|
||||
|---|---|
|
||||
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
|
||||
| Extrude & Bevel, Inflate, Rotate | initial: live, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, groups and layers turned about one shared axis (#4506), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
|
||||
| Materials panel, lighting, ray-traced render | one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; no materials, shadows or ray tracing |
|
||||
| Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; no materials, shadows or ray tracing |
|
||||
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | none |
|
||||
| Turntable (30.0 beta) | none |
|
||||
| Export 3D objects (OBJ, USDA, glTF) | none |
|
||||
|
||||
+15
@@ -2223,6 +2223,21 @@ The document stores their numeric settings; manual edits remain possible through
|
||||
{"name":"run_command","arguments":{"command":"effect.lightingPreset","params":{"index":0,"item":null,"preset":"topLeft"}}}
|
||||
```
|
||||
|
||||
## 3D object type
|
||||
|
||||
`effect.setParams {index: int, threeDType: "plane" | "extrude" | "revolve" | "inflate", params?, item?, ids?, aligned?}`
|
||||
changes an applied 3D effect into Rotate, Extrude & Bevel, Revolve or Inflate in place, as the object types of
|
||||
**Window › 3D and Materials** do ([3D and Materials panel](three-d.md#3d-and-materials-panel)). The settings both
|
||||
effects take keep their values (rotation angles are wrapped into -180..180 when the new type is not Revolve), the
|
||||
settings only the new effect takes start at its defaults, and `params` then merge into the new effect's settings,
|
||||
checked as that effect's. It is one undo step. Any other `threeDType` value, or an effect at `index` that is not a
|
||||
3D effect, is an error and changes nothing. The type the effect already has, with no `params`, `visible` or
|
||||
`warpStyle`, changes nothing, records no undo step and returns `{ids: []}`.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"threeDType":"inflate","params":{"volume":70}}}}
|
||||
```
|
||||
|
||||
## Extrude & Bevel, Inflate and Rotate
|
||||
|
||||
`effect.apply {effect: "threeD.extrude" | "threeD.inflate" | "threeD.rotate", params: {...}, ids?, item?}` applies
|
||||
|
||||
@@ -519,6 +519,7 @@ since 2026-10-01.
|
||||
## Evidence: what ships today, by area
|
||||
|
||||
- 3D lighting presets: `crates/effects/src/lighting_presets.rs`, `crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs` and the pointer tests in `crates/ui-egui/src/dialogs/revolve/tests.rs` cover recognition, atomic targeting, native round trips, preview, Cancel, reset and Undo.
|
||||
- Window › 3D and Materials panel: `crates/ui-egui/src/panels/three_d.rs` applies a 3D effect by object type (Plane, Extrude, Revolve, Inflate), changes an applied one in place (`effect.setParams` `threeDType`) and edits it with the options dialog's Object, Rotation and Lighting controls, one undo step per change; `crates/engine/src/cmd/effectcmd/three_d_type_tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. No Materials tab.
|
||||
|
||||
- **Status-bar pointer readout (2026-10-18):** uses the same effective ruler origin as the position fields, then formats the relative X/Y in General units. The absolute pointer position, document, selection, Undo/Redo and saved state are preserved. Three `chrome::tests::status_coordinates_*` painted-text regressions cover zero/positive/negative origins, unit conversion, global/artboard mode, active artboard, reset and missing pointer/document. Original headless status-bar captures were reviewed before and after the change. Scores and hours are unchanged.
|
||||
|
||||
|
||||
+30
-1
@@ -14,7 +14,36 @@ Select a path or compound path, then choose **Effect › 3D and Materials › Ex
|
||||
artboard.
|
||||
|
||||
Preview changes are temporary until OK; Cancel restores the document; OK is one undo step. An object takes one
|
||||
3D effect: edit its options in the Appearance panel, or remove it before applying another.
|
||||
3D effect: edit its options in the Appearance panel or the 3D and Materials panel, or remove it before applying
|
||||
another.
|
||||
|
||||
## 3D and Materials panel
|
||||
|
||||
**Window › 3D and Materials** opens a panel that edits the 3D effect of the selection without a dialog, as the
|
||||
public [3D and Materials panel options](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/3d-materials-panel-options.html)
|
||||
and [Create 3D vector artwork](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-vector-artwork.html)
|
||||
pages describe it.
|
||||
|
||||
- The object types **Plane**, **Extrude**, **Revolve** and **Inflate** sit at the top. A type applies its effect
|
||||
to each selected object that has no 3D effect (Plane is Rotate, Extrude is Extrude & Bevel), and changes the
|
||||
3D effect of each other selected object in place (`effect.setParams` with `threeDType`): the settings both
|
||||
effects take keep their values, and the rest start at the new effect's defaults. One click is one undo step,
|
||||
and an object whose effect already has the type does not change.
|
||||
- Below them are the Object, Rotation and Lighting tabs of the effect's options dialog, with the same controls
|
||||
and ranges, showing the 3D effect of the first selected object (its own effects first, then those of its fills
|
||||
and strokes). A changed setting goes to the 3D effect of each selected object whose effect takes that setting;
|
||||
other effects and objects without a 3D effect do not change. A setting the selected 3D effects hold with
|
||||
different values shows its default with its reset dot filled; a checkbox shows a dash instead, and a mixed
|
||||
Shading leaves the light settings available. Its reset dot, a choice from its menu or a click on its checkbox
|
||||
(which turns a mixed one on) sets it on all of them; a typed or dragged number sets it when it differs from the
|
||||
default shown. Other changes leave it as each effect has it.
|
||||
The list of lights shows the first object's lights, and an edit to it (Add Light, Delete Light, or a setting
|
||||
of one light in a list) reaches only the objects whose list of lights matches the first object's. Add Light is
|
||||
unavailable while a setting of a single light differs between the selected effects.
|
||||
- Each change is one undo step. Dragging a value or a slider previews it on the artboard and records one step
|
||||
when the pointer is released; a typed value is recorded when it is entered. Drawing the panel records nothing.
|
||||
- The Materials tab, shadows and ray-traced rendering are not built (see below). The rotation and light rings on
|
||||
the artboard are part of the options dialog only.
|
||||
|
||||
## Extrude & Bevel
|
||||
|
||||
|
||||
Reference in New Issue
Block a user