feat(ui): edit independent rectangle corner radii with narrow-panel controls and history tests

This commit is contained in:
Nigel Ihza Firdausy
2026-10-09 08:55:20 +07:00
committed by Claude
parent a9fbeb0447
commit 34183af287
12 changed files with 136 additions and 16 deletions
+4
View File
@@ -25,6 +25,10 @@
Layer Vrstva
Type Text
Select Výběr
Top Left Vlevo nahoře
Top Right Vpravo nahoře
Bottom Left Vlevo dole
Bottom Right Vpravo dole
Filter Filtr
View Zobrazení
Window Okno
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -21,6 +21,10 @@
Layer Ebene
Type Text
Select Auswahl
Top Left Oben links
Top Right Oben rechts
Bottom Left Unten links
Bottom Right Unten rechts
Filter Filter
View Ansicht
Window Fenster
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -17,6 +17,10 @@
Layer Capa
Type Texto
Select Selección
Top Left Superior izquierda
Top Right Superior derecha
Bottom Left Inferior izquierda
Bottom Right Inferior derecha
Filter Filtro
View Ver
Window Ventana
Can't render this file because it contains an unexpected character in line 4 and column 47.
+4
View File
@@ -24,6 +24,10 @@
Layer Calque
Type Texte
Select Sélection
Top Left En haut à gauche
Top Right En haut à droite
Bottom Left En bas à gauche
Bottom Right En bas à droite
Filter Filtre
View Affichage
Window Fenêtre
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -17,6 +17,10 @@
Layer Layer
Type Teks
Select Seleksi
Top Left Kiri atas
Top Right Kanan atas
Bottom Left Kiri bawah
Bottom Right Kanan bawah
Filter Filter
View Tampilan
Window Jendela
Can't render this file because it contains an unexpected character in line 4 and column 47.
+4
View File
@@ -17,6 +17,10 @@
Layer Livello
Type Testo
Select Selezione
Top Left In alto a sinistra
Top Right In alto a destra
Bottom Left In basso a sinistra
Bottom Right In basso a destra
Filter Filtro
View Vista
Window Finestra
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
17
18
19
20
21
22
23
24
25
26
+4
View File
@@ -21,6 +21,10 @@
Layer レイヤー
Type 文字
Select 選択
Top Left 左上
Top Right 右上
Bottom Left 左下
Bottom Right 右下
Filter フィルター
View 表示
Window ウィンドウ
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -21,6 +21,10 @@
Layer Camada
Type Tipo
Select Selecionar
Top Left Superior esquerdo
Top Right Superior direito
Bottom Left Inferior esquerdo
Bottom Right Inferior direito
Filter Filtro
View Visualizar
Window Janela
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -24,6 +24,10 @@
Layer Слой
Type Текст
Select Выделение
Top Left Верхний левый
Top Right Верхний правый
Bottom Left Нижний левый
Bottom Right Нижний правый
Filter Фильтр
View Просмотр
Window Окно
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -17,6 +17,10 @@
Layer 图层
Type 文字
Select 选择
Top Left 左上
Top Right 右上
Bottom Left 左下
Bottom Right 右下
Filter 滤镜
View 视图
Window 窗口
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
17
18
19
20
21
22
23
24
25
26
+4
View File
@@ -25,6 +25,10 @@
Layer 圖層
Type 文字
Select 選取
Top Left 左上
Top Right 右上
Bottom Left 左下
Bottom Right 右下
Filter 濾鏡
View 檢視
Window 視窗
Can't render this file because it contains an unexpected character in line 6 and column 47.
+92 -16
View File
@@ -602,6 +602,16 @@ fn swatch(ui: &mut egui::Ui, fill: Option<&photocraft_doc::Fill>, tip: &str) ->
out
}
/// One corner of the live rectangle, preserving the other three radii.
/// The engine takes the full [top-left, top-right, bottom-right, bottom-left] array.
fn corner_radii_patch(radii: [f64; 4], corner: usize, radius: f32) -> Value {
let mut next = radii;
if let Some(r) = next.get_mut(corner) {
*r = f64::from(radius.max(0.0));
}
json!({"radii": next})
}
/// Properties panel for a shape layer: Appearance (fill, stroke) and live shape geometry.
pub fn shape_properties(app: &mut PhotocraftApp, ui: &mut egui::Ui, id: photocraft_doc::LayerId) {
let Some(sh) = app.session.active().and_then(|s| s.doc.layer(id)).and_then(|l| match &l.content {
@@ -669,28 +679,49 @@ pub fn shape_properties(app: &mut PhotocraftApp, ui: &mut egui::Ui, id: photocra
}
photocraft_doc::vector::LiveShape::Line { .. } => {}
}
ui.horizontal(|ui| match live {
ui.vertical(|ui| match live {
photocraft_doc::vector::LiveShape::Rect { radii, .. } => {
let mut r = radii[0] as f32;
if num(ui, tl!("Corner radius"), &mut r, 0.0..=100000.0, "px") {
edit = Some(json!({"radii": r, "coalesce": key("radius")}));
// Keep the uniform workflow available after independent corner edits too.
ui.horizontal(|ui| {
let mut all = radii[0] as f32;
if num(ui, tl!("Corner radius"), &mut all, 0.0..=100000.0, "px") {
edit = Some(json!({"radii": all, "coalesce": key("radius-all")}));
}
});
// Separate rows also fit the narrow Properties dock. The labels use the
// localized corner names, rather than untranslated abbreviations.
for (index, label) in [(0, tl!("Top Left")), (1, tl!("Top Right")), (3, tl!("Bottom Left")), (2, tl!("Bottom Right"))] {
ui.horizontal(|ui| {
let mut radius = radii[index] as f32;
if num(ui, label, &mut radius, 0.0..=100000.0, "px") {
let mut patch = corner_radii_patch(*radii, index, radius);
patch["coalesce"] = json!(key(&format!("radius-{index}")));
edit = Some(patch);
}
});
}
}
photocraft_doc::vector::LiveShape::Polygon { sides, star_ratio, .. } => {
let mut n = *sides as f32;
if num(ui, tl!("Sides"), &mut n, 3.0..=100.0, "") {
edit = Some(json!({"sides": n.round() as u32, "coalesce": key("sides")}));
}
let mut sr = (*star_ratio * 100.0) as f32;
if num(ui, tl!("Star ratio"), &mut sr, 1.0..=100.0, "%") {
edit = Some(json!({"starRatio": sr as f64 / 100.0, "coalesce": key("star")}));
}
ui.horizontal(|ui| {
let mut n = *sides as f32;
if num(ui, tl!("Sides"), &mut n, 3.0..=100.0, "") {
edit = Some(json!({"sides": n.round() as u32, "coalesce": key("sides")}));
}
let mut sr = (*star_ratio * 100.0) as f32;
if num(ui, tl!("Star ratio"), &mut sr, 1.0..=100.0, "%") {
edit = Some(json!({"starRatio": sr as f64 / 100.0, "coalesce": key("star")}));
}
})
.inner
}
photocraft_doc::vector::LiveShape::Line { weight, .. } => {
let mut w = *weight as f32;
if num(ui, tl!("Weight"), &mut w, 1.0..=10000.0, "px") {
edit = Some(json!({"weight": w, "coalesce": key("weight")}));
}
ui.horizontal(|ui| {
let mut w = *weight as f32;
if num(ui, tl!("Weight"), &mut w, 1.0..=10000.0, "px") {
edit = Some(json!({"weight": w, "coalesce": key("weight")}));
}
})
.inner
}
_ => {}
});
@@ -1200,6 +1231,51 @@ mod tests {
assert!(!legacy.dragging && !legacy.adjusting_last);
}
/// #1520: changing one corner through the Properties command must keep the other three
/// independent, remain a live rectangle, and undo/redo as one geometry edit.
#[test]
fn live_rectangle_corner_radius_edit_preserves_other_corners_and_history() {
let mut app = app();
let created = app.run("shape.create", json!({"kind": "roundedRect", "rect": [10, 10, 150, 100], "radii": [5, 10, 15, 20]})).unwrap();
let id = created["layer"].as_u64().unwrap();
let get_radii = |app: &PhotocraftApp| {
let st = app.session.active().unwrap();
let shape = st.doc.layer(photocraft_doc::LayerId(id)).unwrap();
match &shape.content {
LayerContent::Shape(sh) => match &sh.live {
Some(photocraft_doc::vector::LiveShape::Rect { radii, .. }) => *radii,
_ => panic!("rectangle must remain a live shape"),
},
_ => panic!("expected shape layer"),
}
};
assert_eq!(get_radii(&app), [5.0, 10.0, 15.0, 20.0]);
let mut patch = corner_radii_patch(get_radii(&app), 1, 30.0);
patch["layer"] = json!(id);
app.run("shape.edit", patch).unwrap();
assert_eq!(get_radii(&app), [5.0, 30.0, 15.0, 20.0]);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(get_radii(&app), [5.0, 10.0, 15.0, 20.0]);
app.run("edit.redo", json!({})).unwrap();
assert_eq!(get_radii(&app), [5.0, 30.0, 15.0, 20.0]);
for corner in 0..4 {
let before = get_radii(&app);
let radius = 35.0 + corner as f32;
let mut expected = before;
expected[corner] = f64::from(radius);
let mut patch = corner_radii_patch(before, corner, radius);
patch["layer"] = json!(id);
app.run("shape.edit", patch).unwrap();
assert_eq!(get_radii(&app), expected);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(get_radii(&app), before);
app.run("edit.redo", json!({})).unwrap();
assert_eq!(get_radii(&app), expected);
}
app.run("shape.edit", json!({"layer": id, "radii": 12})).unwrap();
assert_eq!(get_radii(&app), [12.0; 4]);
}
/// #534: dragging in a shape's fill picker, opened from the Properties panel at the right edge
/// of the window, keeps the picker in place (it flipped from side to side as its width
/// followed the colour readouts, which moved it under the pointer).