Fix #40: W/H constrain-proportions link in the Properties panel and Control bar
The Transform panel had the link, but kept it in panel-local egui memory; the Properties panel's Transform section and the Control bar had none, so size changes there were never proportional. The link is now the constrainProportions preference (prefs.get/prefs.set), one toggle shared by the three places (transform::constrain_link), and every W/H field passes it to object.setBounds as `proportional`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6780ffae60
commit
6992c843b4
|
|
@ -320,6 +320,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
|||
p!("appendConverted", "File Handling", "Files", "Mark Older Files as [Converted] When Opened", bool),
|
||||
// Native saves (`document.save {compress}`)
|
||||
p!("useCompression", "File Handling", "Files", "Use Compression", bool),
|
||||
// The W/H link of the Transform panel, the Properties panel and the Control bar
|
||||
p!("constrainProportions", "General", "Transform Panel", "Constrain Width and Height Proportions", bool),
|
||||
];
|
||||
|
||||
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
|
||||
|
|
|
|||
|
|
@ -472,6 +472,10 @@ pub struct Prefs {
|
|||
/// Preferences dialog field: resetting the preferences keeps it; `print.presets.*` edit it.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub print_presets: Vec<cmd::printpresets::PrintPreset>,
|
||||
/// Constrain Width and Height Proportions: the link between W and H in the Transform panel,
|
||||
/// the Properties panel and the Control bar (their size fields pass `proportional` to
|
||||
/// `object.setBounds`).
|
||||
pub constrain_proportions: bool,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
|
|
@ -604,6 +608,7 @@ impl Default for Prefs {
|
|||
web_export_presets: vec![],
|
||||
web_export_settings: None,
|
||||
print_presets: vec![],
|
||||
constrain_proportions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,10 +203,15 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
// The bounding box, rotated with rotated objects: its centre and its own sides.
|
||||
if let Some(b) = app.selection_box() {
|
||||
let c = b.center();
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
for (k, lbl, v) in [("x", "X:", c.x), ("y", "Y:", c.y), ("width", "W:", b.rect.width()), ("height", "H:", b.rect.height())] {
|
||||
// The W/H link sits between W and H.
|
||||
if k == "height" {
|
||||
crate::panels::transform::constrain_link(app, ui);
|
||||
}
|
||||
ui.label(egui::RichText::new(lbl).size(12.0).color(t.text_dim));
|
||||
if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v), units, 80.0) {
|
||||
app.run("object.setBounds", json!({k: nv, "reference": 4})).ok();
|
||||
app.run("object.setBounds", json!({k: nv, "reference": 4, "proportional": link})).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -455,6 +455,8 @@ mod tests_appearance;
|
|||
#[cfg(test)]
|
||||
mod tests_asset_export;
|
||||
#[cfg(test)]
|
||||
mod tests_constrain;
|
||||
#[cfg(test)]
|
||||
mod tests_css_properties;
|
||||
#[cfg(test)]
|
||||
mod tests_effectedit;
|
||||
|
|
|
|||
|
|
@ -268,7 +268,9 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
ui.data_mut(|d| d.insert_temp(egui::Id::new("refpt"), i));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let fw = ((ui.available_width() - 44.0) / 2.0).clamp(60.0, 110.0);
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
// Room for the labels and the W/H link.
|
||||
let fw = ((ui.available_width() - 70.0) / 2.0).clamp(60.0, 110.0);
|
||||
egui::Grid::new("xf-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
dim_label(ui, "X:");
|
||||
if let Some(v) = widgets::num_field(ui, "tx", Some(rp.x), units, fw) {
|
||||
|
|
@ -276,7 +278,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
dim_label(ui, "W:");
|
||||
if let Some(v) = widgets::num_field(ui, "tw", Some(b.width()), units, fw) {
|
||||
app.run("object.setBounds", json!({"width": v, "reference": refi})).ok();
|
||||
app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link})).ok();
|
||||
}
|
||||
ui.end_row();
|
||||
dim_label(ui, "Y:");
|
||||
|
|
@ -285,10 +287,11 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
dim_label(ui, "H:");
|
||||
if let Some(v) = widgets::num_field(ui, "th", Some(b.height()), units, fw) {
|
||||
app.run("object.setBounds", json!({"height": v, "reference": refi})).ok();
|
||||
app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link})).ok();
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
super::transform::constrain_link(app, ui);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
icons::icon(ui, "rotate-ccw", 16.0, Tokens::get(ui.ctx()).icon);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
//! The W/H link (Constrain Width and Height Proportions) in the Transform panel, the Properties
|
||||
//! panel's Transform section and the Control bar: one preference, passed on as `proportional`.
|
||||
|
||||
use egui::{Event, Key, Modifiers, PointerButton};
|
||||
use serde_json::json;
|
||||
|
||||
use super::tests_appearance::{frame_raw, run};
|
||||
use super::*;
|
||||
|
||||
/// What draws a set of size fields.
|
||||
type Draw = fn(&mut VectorcraftApp, &mut Ui);
|
||||
|
||||
/// A selected 100 × 50 pt rectangle.
|
||||
fn rect_app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
run(&mut app, "file.new", json!({"width": 400, "height": 400}));
|
||||
let id = run(&mut app, "shape.rectangle", json!({"x": 10, "y": 10, "width": 100, "height": 50}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({ "ids": [id] }));
|
||||
app
|
||||
}
|
||||
|
||||
fn size(app: &VectorcraftApp) -> (f64, f64) {
|
||||
let st = app.session.active().unwrap();
|
||||
let b = st.doc.bounds_of(&st.selection.objects, false).unwrap();
|
||||
(b.width().round(), b.height().round())
|
||||
}
|
||||
|
||||
/// Type `value` into the W field (the one showing "100 pt") of what `draw` shows, and press Enter.
|
||||
fn set_width(app: &mut VectorcraftApp, draw: Draw, value: &str) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut VectorcraftApp, events: Vec<Event>| {
|
||||
time += 1.0;
|
||||
frame_raw(&ctx, app, egui::RawInput { events, time: Some(time), ..Default::default() }, draw)
|
||||
};
|
||||
let shown = frame(app, vec![]);
|
||||
let at = shown.iter().find(|(s, _)| s == "100 pt").unwrap_or_else(|| panic!("no W field in {shown:?}")).1.center();
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
frame(app, vec![Event::PointerMoved(at), button(true), button(false)]);
|
||||
// Focus selected the whole field: typing replaces it.
|
||||
frame(app, vec![]);
|
||||
frame(app, vec![Event::Text(value.into())]);
|
||||
let enter = Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
|
||||
frame(app, vec![enter]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_link_keeps_proportions_in_every_size_field() {
|
||||
let surfaces: [(&str, Draw); 3] =
|
||||
[("Transform panel", transform::show), ("Properties", properties::transform_section), ("Control bar", crate::chrome::control_bar)];
|
||||
for (name, draw) in surfaces {
|
||||
// Off: W alone.
|
||||
let mut app = rect_app();
|
||||
set_width(&mut app, draw, "200");
|
||||
assert_eq!(size(&app), (200.0, 50.0), "{name}, unlinked");
|
||||
// On: H follows.
|
||||
let mut app = rect_app();
|
||||
run(&mut app, "prefs.set", json!({"key": "constrainProportions", "value": true}));
|
||||
set_width(&mut app, draw, "200");
|
||||
assert_eq!(size(&app), (200.0, 100.0), "{name}, linked");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_link_is_a_preference_agents_can_read_and_set() {
|
||||
let mut app = rect_app();
|
||||
assert_eq!(run(&mut app, "prefs.get", json!({"key": "constrainProportions"})), json!(false));
|
||||
run(&mut app, "prefs.set", json!({"key": "constrainProportions", "value": true}));
|
||||
assert!(app.session.prefs.constrain_proportions);
|
||||
// What the fields send with the link on.
|
||||
run(&mut app, "object.setBounds", json!({"height": 100, "reference": 4, "proportional": true}));
|
||||
assert_eq!(size(&app), (200.0, 100.0));
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let bx = app.selection_box();
|
||||
let bounds = bx.map(|b| b.rect);
|
||||
let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4);
|
||||
let link: bool = pstate(ui.ctx(), "xf-link");
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
let has = bounds.is_some();
|
||||
let rp = bx.map(|b| b.reference_point(refi));
|
||||
ui.horizontal(|ui| {
|
||||
|
|
@ -63,9 +63,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
ui.end_row();
|
||||
});
|
||||
});
|
||||
if widgets::icon_button(ui, if link { "link" } else { "link-2-off" }, "Constrain Width and Height Proportions", link, 22.0).clicked() {
|
||||
set_pstate(ui.ctx(), "xf-link", !link);
|
||||
}
|
||||
constrain_link(app, ui);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let origin = rp.map(|p| json!([p.x, p.y]));
|
||||
|
|
@ -139,6 +137,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
|
||||
/// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the
|
||||
/// `constrainProportions` preference, which the size fields pass on as `proportional`.
|
||||
pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let on = app.session.prefs.constrain_proportions;
|
||||
if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, "Constrain Width and Height Proportions", on, 22.0).clicked() {
|
||||
set_pref(app, "constrainProportions", !on);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a boolean preference (through `prefs.set`, as agents do).
|
||||
pub fn set_pref(app: &mut VectorcraftApp, key: &str, on: bool) {
|
||||
if let Err(e) = app.run("prefs.set", json!({ "key": key, "value": on })) {
|
||||
|
|
|
|||
11
docs/mcp.md
11
docs/mcp.md
|
|
@ -1667,3 +1667,14 @@ its undo history are as if the click never happened. Commands on the edited text
|
|||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.discardEmpty","params":{"id":42}}}
|
||||
```
|
||||
|
||||
## Constrain proportions
|
||||
|
||||
The link between W and H in the Transform panel, the Properties panel and the Control bar is the
|
||||
`constrainProportions` preference (`prefs.get` / `prefs.set`). With it on, those fields send `proportional: true`
|
||||
to `object.setBounds`, which scales the other dimension by the same factor; agents pass `proportional` themselves.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"constrainProportions","value":true}}}
|
||||
{"name":"run_command","arguments":{"command":"object.setBounds","params":{"width":200,"proportional":true}}}
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue