mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 10:47:27 +00:00
3D and Materials: a mixed checkbox shows a dash and keeps each effect's value
With several objects selected, Shading, Revolve's Cap, Keep visible surfaces only when expanding and the other checkboxes read a setting the effects hold with different values as off. A mixed Shading also turned off the Lighting tab's controls. - A mixed checkbox shows a dash with its reset dot filled; a click turns it on for every selected effect, and other changes leave it as each effect has it. - A mixed Shading leaves the light settings available. - A mixed menu choice shows its default; a mixed light color fills its reset dot. - Add Light is unavailable while a setting of a single light is mixed.
This commit is contained in:
@@ -98,14 +98,13 @@ pub(crate) 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));
|
||||
}
|
||||
});
|
||||
@@ -123,17 +122,17 @@ pub(crate) fn controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog)
|
||||
}
|
||||
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));
|
||||
}
|
||||
});
|
||||
@@ -253,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);
|
||||
@@ -266,39 +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));
|
||||
let mixed = d.fields.get(key).is_some_and(Value::is_null);
|
||||
if reset_dot(ui, key, mixed || 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);
|
||||
@@ -419,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 {
|
||||
@@ -464,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"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -337,6 +337,70 @@ fn a_drag_ends_when_the_panel_is_no_longer_drawn() {
|
||||
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"});
|
||||
|
||||
@@ -246,7 +246,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
/// 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
|
||||
/// with its reset dot filled, and its reset dot, a menu choice, a checkbox click, or a number other
|
||||
/// (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>) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+6
-4
@@ -33,11 +33,13 @@ pages describe it.
|
||||
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. Its reset dot, a choice from its menu or a click
|
||||
on its checkbox sets it on all of them; a typed or dragged number sets it when it differs from the default
|
||||
shown.
|
||||
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.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user