From 78f0297127694b0d5570626700ed122bdd7303f8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 03:00:15 +0700 Subject: [PATCH] Part of #812: an ellipse's Pie Start and End Angle and Invert Pie An ellipse kept pie angles in its model, but nothing drew or set them. Now the Transform and Properties panels show Ellipse Properties with Pie Start Angle, Pie End Angle and Invert Pie, as in Illustrator, and object.setLiveShape takes {pieStart, pieEnd, invertPie}. The pie runs counterclockwise on screen from the start angle (0 at 3 o'clock) to the end angle and closes through the centre; 0 to 360 is the whole ellipse, an end of 0 means 360, and Invert Pie swaps the angles to show the other part. One undo step each; a bad angle is an error. The labels are translated in every complete language. The on-canvas pie widget is still to come. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/doc/src/node.rs | 2 +- crates/engine/src/cmd/object.rs | 28 ++++++++++- crates/engine/src/tests_livecorners.rs | 28 +++++++++++ crates/geom/src/shapes.rs | 65 +++++++++++++++++++++++++ crates/ui-egui/src/i18n/es.tsv | 4 ++ crates/ui-egui/src/i18n/fr.tsv | 4 ++ crates/ui-egui/src/i18n/it.tsv | 4 ++ crates/ui-egui/src/i18n/ja.tsv | 4 ++ crates/ui-egui/src/i18n/ru.tsv | 4 ++ crates/ui-egui/src/i18n/uk.tsv | 4 ++ crates/ui-egui/src/i18n/zh-hans.tsv | 4 ++ crates/ui-egui/src/i18n/zh-hant.tsv | 4 ++ crates/ui-egui/src/panels/properties.rs | 1 + crates/ui-egui/src/panels/transform.rs | 20 ++++++++ 15 files changed, 175 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6554d0ab..12a2cfc7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist Clear deletes individual selected compound-path members without deleting their siblings, and explicit object ids work without a selection; Cut still removes the same objects it copies (#786). - **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: Update Selection and the Layers panel's saved-selection menu. - **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. - - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). + - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812; the on-canvas pie widget is still to come). - **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes). - **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres; Alt-click opens the tool's dialog, and a drag with Alt down at the release, from the press or pressed mid-drag, transforms a copy), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag). - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two straight sides meet at an anchor. Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply. diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs index 665279b6..464f40c7 100644 --- a/crates/doc/src/node.rs +++ b/crates/doc/src/node.rs @@ -146,7 +146,7 @@ impl LiveShape { LiveShape::Rectangle { w, h, radii, kinds, xf } => { shapes::rectangle_with_corners(Rect::new(0.0, 0.0, *w, *h), *radii, *kinds).transformed(*xf) } - LiveShape::Ellipse { w, h, xf, .. } => shapes::ellipse(Rect::new(0.0, 0.0, *w, *h)).transformed(*xf), + LiveShape::Ellipse { w, h, pie, xf } => shapes::ellipse_pie(Rect::new(0.0, 0.0, *w, *h), pie.0, pie.1).transformed(*xf), LiveShape::Polygon { radii, kinds, .. } => cut_corners(&self.polygon_outline(), radii, kinds).0, LiveShape::Line { a, b } => shapes::line(*a, *b), LiveShape::Path { base, radii, kinds } => cut_corners(base, radii, kinds).0, diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index ffc9b007..7ef85be9 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -196,7 +196,7 @@ pub fn specs() -> Vec { "Live Shape Properties", [], None, - "{id?, ids?, radius?: pt, kind?: \"round\"|\"invertedRound\"|\"chamfer\", corners?: [i…], sides?: n} (Live Corners on any path: radius and kind set its corners (anchors without handles between two straight sides): `corners` (anchor indices of the path with its corners uncut, counting every subpath's anchors in order: a rectangle's 0 top-left, 1 top-right, 2 bottom-right, 3 bottom-left; a polygon's from the first vertex clockwise), else the corners with a Direct-Selected anchor, else every corner; each radius is drawn no larger than half the corner's shorter side allows; a path that isn't a live shape keeps its uncut outline so its corners stay editable, and is plain again once none is cut; sides: a polygon's, which keep the radius they shared)", + "{id?, ids?, radius?: pt, kind?: \"round\"|\"invertedRound\"|\"chamfer\", corners?: [i…], sides?: n, pieStart?, pieEnd?: degrees (an ellipse's pie, counterclockwise from 3 o'clock; 0 to 360 is the whole ellipse), invertPie?: true (swaps them: the other part of the ellipse)} (Live Corners on any path: radius and kind set its corners (anchors without handles between two straight sides): `corners` (anchor indices of the path with its corners uncut, counting every subpath's anchors in order: a rectangle's 0 top-left, 1 top-right, 2 bottom-right, 3 bottom-left; a polygon's from the first vertex clockwise), else the corners with a Direct-Selected anchor, else every corner; each radius is drawn no larger than half the corner's shorter side allows; a path that isn't a live shape keeps its uncut outline so its corners stay editable, and is plain again once none is cut; sides: a polygon's, which keep the radius they shared)", has_selection, set_live_shape ), @@ -1095,6 +1095,19 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result { .transpose()?; let radius = p.get("radius").and_then(Value::as_f64); let sides = p.get("sides").and_then(Value::as_u64); + // An ellipse's pie (Ellipse Properties: Pie Start and End Angle, degrees; Invert Pie swaps them). + let angle = |k: &str| -> Result> { + match p.get(k) { + None | Some(Value::Null) => Ok(None), + Some(v) => v + .as_f64() + .filter(|a| a.is_finite()) + .map(|a| Some(a.rem_euclid(360.0))) + .ok_or_else(|| bad("object.setLiveShape", format!("`{k}` must be an angle in degrees, not {v}"))), + } + }; + let (pie_start, pie_end) = (angle("pieStart")?, angle("pieEnd")?); + let invert = p.get("invertPie").and_then(Value::as_bool) == Some(true); s.edit("Live Shape", |d, sel| { for id in &ids { let Some(NodeKind::Path { path, live, .. }) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; @@ -1102,6 +1115,19 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result { l.set_sides(n); *path = l.to_path(); } + if let Some(l @ LiveShape::Ellipse { .. }) = live.as_mut() + && (pie_start.is_some() || pie_end.is_some() || invert) + { + if let LiveShape::Ellipse { pie, .. } = l { + // An end of 0 is a full turn (Illustrator shows a whole ellipse as 0° to 360°). + let end_of = |a: f64| if a == 0.0 { 360.0 } else { a }; + *pie = (pie_start.unwrap_or(pie.0), pie_end.map_or(pie.1, end_of)); + if invert { + *pie = (pie.1.rem_euclid(360.0), end_of(pie.0)); + } + } + *path = l.to_path(); + } if radius.is_none() && kind.is_none() { continue; } diff --git a/crates/engine/src/tests_livecorners.rs b/crates/engine/src/tests_livecorners.rs index 8988e3c9..023108f8 100644 --- a/crates/engine/src/tests_livecorners.rs +++ b/crates/engine/src/tests_livecorners.rs @@ -279,3 +279,31 @@ fn a_pen_path_rounds_its_corners_but_not_its_ends() { run(&mut s, json!({"id": e.0, "radius": 8})); assert_eq!(node_path(&s, e), before); } + +/// #812: an ellipse's Pie Start and End Angle cut it as a pie; Invert Pie shows the other part; +/// one undo step each; a bad angle is an error. +#[test] +fn an_ellipse_takes_pie_angles_and_inverts() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap(); + let id = NodeId(s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 200, "height": 100})).unwrap()["id"].as_u64().unwrap()); + let pie = |s: &Session| match &s.doc().unwrap().doc.node(id).unwrap().kind { + NodeKind::Path { live: Some(LiveShape::Ellipse { pie, .. }), path, .. } => (*pie, path.bounds().unwrap()), + k => panic!("{k:?}"), + }; + s.execute("object.setLiveShape", &json!({"pieStart": 0, "pieEnd": 90})).unwrap(); + let (p, b) = pie(&s); + assert_eq!(p, (0.0, 90.0)); + assert!((b.x0 - 100.0).abs() < 1e-6 && (b.y1 - 50.0).abs() < 1e-6, "the quarter up and right of the centre: {b:?}"); + s.execute("object.setLiveShape", &json!({"invertPie": true})).unwrap(); + let (p, b) = pie(&s); + assert_eq!(p, (90.0, 360.0)); + assert!(b.x0.abs() < 1e-6 && (b.y1 - 100.0).abs() < 1e-6, "the other three quarters: {b:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(pie(&s).0, (0.0, 90.0)); + // 0 to 360 (or an end of 0) is the whole ellipse again. + s.execute("object.setLiveShape", &json!({"pieStart": 0, "pieEnd": 0})).unwrap(); + assert_eq!(pie(&s).0, (0.0, 360.0)); + assert_eq!(s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().anchor_count(), 4); + assert!(s.execute("object.setLiveShape", &json!({"pieStart": "half"})).is_err()); +} diff --git a/crates/geom/src/shapes.rs b/crates/geom/src/shapes.rs index 756923cd..64db3682 100644 --- a/crates/geom/src/shapes.rs +++ b/crates/geom/src/shapes.rs @@ -86,6 +86,50 @@ pub fn ellipse(r: Rect) -> PathData { PathData::single(SubPath::new(anchors, true)) } +/// The ellipse inscribed in `r` cut as a pie: from `start` to `end` degrees, measured +/// counterclockwise on screen from 3 o'clock (as Illustrator's Pie Start and End Angle are), then +/// back through the centre. A pie spanning the whole turn (or nothing) is the whole [`ellipse`]. +/// The arc is one cubic per quarter turn at most: the centre, then the arc's anchors. +pub fn ellipse_pie(r: Rect, start: f64, end: f64) -> PathData { + let span = (end - start).rem_euclid(360.0); + if !(start.is_finite() && end.is_finite()) || span < 1e-9 || 360.0 - span < 1e-9 { + return ellipse(r); + } + let r = r.abs(); + let c = r.center(); + let (rx, ry) = (r.width() / 2.0, r.height() / 2.0); + // Screen y runs down: counterclockwise on screen is a falling y. + let at = |deg: f64| { + let t = deg.to_radians(); + Point::new(c.x + rx * t.cos(), c.y - ry * t.sin()) + }; + let tangent = |deg: f64| { + let t = deg.to_radians(); + Vec2::new(-rx * t.sin(), -ry * t.cos()) + }; + let pieces = if span <= 90.0 { + 1 + } else if span <= 180.0 { + 2 + } else if span <= 270.0 { + 3 + } else { + 4 + }; + let step = span / f64::from(pieces); + let k = 4.0 / 3.0 * (step.to_radians() / 4.0).tan(); + let mut anchors = vec![Anchor::corner(c)]; + for i in 0..=pieces { + let a = start + step * f64::from(i); + let (p, d) = (at(a), tangent(a) * k); + let h_in = if i == 0 { p } else { p - d }; + let h_out = if i == pieces { p } else { p + d }; + let kind = if i == 0 || i == pieces { AnchorKind::Corner } else { AnchorKind::Smooth }; + anchors.push(Anchor { p, h_in, h_out, kind }); + } + PathData::single(SubPath::new(anchors, true)) +} + /// Regular polygon with `sides` (≥ 3), first vertex straight up from the centre. pub fn polygon(center: Point, radius: f64, sides: u32, rotation_deg: f64) -> PathData { let n = sides.max(3); @@ -207,6 +251,27 @@ mod tests { crate::corners::cut_corners(&rectangle(r), &radii, &[]).1.concat() } + /// A pie is the arc from its start to its end angle, counterclockwise on screen, closed through + /// the centre; a whole turn (or none) is the ellipse. + #[test] + fn a_pie_is_its_arc_closed_through_the_centre() { + let r = Rect::new(0.0, 0.0, 200.0, 100.0); + assert_eq!(ellipse_pie(r, 0.0, 360.0), ellipse(r)); + assert_eq!(ellipse_pie(r, 90.0, 90.0), ellipse(r)); + // A quarter, from 3 o'clock up to 12: the centre, then the arc's two ends. + let q = ellipse_pie(r, 0.0, 90.0); + let a: Vec = q.subpaths[0].anchors.iter().map(|a| a.p).collect(); + assert_eq!(a.len(), 3); + assert!(a[0].distance(Point::new(100.0, 50.0)) < 1e-9 && a[1].distance(Point::new(200.0, 50.0)) < 1e-9); + assert!(a[2].distance(Point::new(100.0, 0.0)) < 1e-9, "counterclockwise on screen: up"); + assert_eq!(q.bounds().map(|b| (b.x0.round(), b.y0.round(), b.x1.round(), b.y1.round())), Some((100.0, 0.0, 200.0, 50.0))); + // Three quarters, from 12 round to 3 o'clock: four pieces at most, the whole box covered. + let big = ellipse_pie(r, 90.0, 0.0); + assert!(big.subpaths[0].anchors.len() <= 6 && big.subpaths[0].closed); + assert_eq!(big.bounds().map(|b| (b.x0.round(), b.y0.round(), b.x1.round(), b.y1.round())), Some((0.0, 0.0, 200.0, 100.0))); + assert!(ellipse_pie(r, f64::NAN, 10.0).anchor_count() == 4, "not a number: the whole ellipse"); + } + #[test] fn rect_is_4_corners_clockwise() { let p = rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)); diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2af7daac..f1f45dc8 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -221,6 +221,10 @@ Auto Size New Area Type Ajustar automáticamente el tamaño del nuevo texto de área Auto Spell Check Revisión ortográfica automática Auto-Collapse Iconic Panels Contraer automáticamente los paneles de iconos + Ellipse Properties: Propiedades de la elipse: + Invert Pie Invertir sector + Pie End Angle: Ángulo final del sector: + Pie Start Angle: Ángulo inicial del sector: Show Tool Group Labels Mostrar etiquetas de grupos de herramientas Auto-Rotate (turn the paper to each artboard) Rotar automáticamente (girar el papel según cada mesa de trabajo) Automatic flatness Lisura automática diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 849006c6..918948f9 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -222,6 +222,10 @@ Auto Size New Area Type Taille automatique du nouveau texte captif Auto Spell Check Vérification orthographique automatique Auto-Collapse Iconic Panels Réduire automatiquement les panneaux en icônes + Ellipse Properties: Propriétés de l’ellipse : + Invert Pie Inverser le secteur + Pie End Angle: Angle de fin du secteur : + Pie Start Angle: Angle de début du secteur : Show Tool Group Labels Afficher les libellés des groupes d'outils Auto-Rotate (turn the paper to each artboard) Rotation automatique (orienter le papier selon chaque plan de travail) Automatic flatness Planéité automatique diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 5a4e89bf..5e5fc0a6 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -221,6 +221,10 @@ Auto Size New Area Type Dimensionamento automatico del nuovo testo in area Auto Spell Check Controllo ortografico automatico Auto-Collapse Iconic Panels Comprimi automaticamente i pannelli a icona + Ellipse Properties: Proprietà dell’ellisse: + Invert Pie Inverti settore + Pie End Angle: Angolo finale del settore: + Pie Start Angle: Angolo iniziale del settore: Show Tool Group Labels Mostra etichette dei gruppi di strumenti Auto-Rotate (turn the paper to each artboard) Rotazione automatica (orienta la carta in base a ogni tavola da disegno) Automatic flatness Planarità automatica diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index fd96d775..269a4dc3 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -345,9 +345,13 @@ Appearance Attribute アピアランス属性 Apply 適用 Blending Mode 描画モード + Ellipse Properties: 楕円形のプロパティ: Fill & Stroke 塗りと線 Fill Color カラー (塗り) + Invert Pie 扇形を反転 Opacity 不透明度 + Pie End Angle: 扇形の終了角度: + Pie Start Angle: 扇形の開始角度: Stroke Color カラー (線) Stroke Weight 線幅 Graphic Style グラフィックスタイル diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 66a53611..22f908f8 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -219,6 +219,10 @@ Auto Size New Area Type Автоматически подгонять размер нового текста в области Auto Spell Check Автоматическая проверка орфографии Auto-Collapse Iconic Panels Автоматически сворачивать значки панелей + Ellipse Properties: Свойства эллипса: + Invert Pie Инвертировать сектор + Pie End Angle: Конечный угол сектора: + Pie Start Angle: Начальный угол сектора: Show Tool Group Labels Показывать названия групп инструментов Auto-Rotate (turn the paper to each artboard) Автоповорот (поворачивать лист по каждой монтажной области) Automatic flatness Автоматическая плоскостность diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b3fe25ae..7df9a0e8 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -215,6 +215,10 @@ Auto Size New Area Type Автоматично підганяти розмір нового тексту в області Auto Spell Check Автоматична перевірка орфографії Auto-Collapse Iconic Panels Автоматично згортати значки панелей + Ellipse Properties: Властивості еліпса: + Invert Pie Інвертувати сектор + Pie End Angle: Кінцевий кут сектора: + Pie Start Angle: Початковий кут сектора: Show Tool Group Labels Показувати назви груп інструментів Auto-Rotate (turn the paper to each artboard) Автоматичний поворот (повертати аркуш відповідно до кожної монтажної області) Automatic flatness Автоматична площинність diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index fb53152a..a0dd0a41 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -204,6 +204,10 @@ Auto Size New Area Type 自动调整新区域文字大小 Auto Spell Check 自动拼写检查 Auto-Collapse Iconic Panels 自动折叠图标面板 + Ellipse Properties: 椭圆属性: + Invert Pie 反转扇形 + Pie End Angle: 扇形终止角度: + Pie Start Angle: 扇形起始角度: Show Tool Group Labels 显示工具组标签 Auto-Rotate (turn the paper to each artboard) 自动旋转 (为每个画板转动纸张) Automatic flatness 自动平滑度 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e273ca6f..3e2be7bf 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -215,6 +215,10 @@ Auto Size New Area Type 自動調整新區域文字大小 Auto Spell Check 自動拼字檢查 Auto-Collapse Iconic Panels 自動收合圖示面板 + Ellipse Properties: 橢圓屬性: + Invert Pie 反轉扇形 + Pie End Angle: 扇形終止角度: + Pie Start Angle: 扇形起始角度: Show Tool Group Labels 顯示工具組標籤 Auto-Rotate (turn the paper to each artboard) 自動旋轉 (將紙張轉向各工作區域) Automatic flatness 自動平滑度 diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index e91be918..ea27ebc8 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -468,6 +468,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { }); corner_radius_row(app, ui, &n, "radius"); } + vectorcraft_doc::LiveShape::Ellipse { pie, .. } => super::transform::pie_rows(app, ui, *pie, "props-pie"), _ => {} } } diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index bed4c97e..bd3a24d5 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -192,6 +192,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); corner_radius_row(app, ui, &n, "xfp-radius"); } + vectorcraft_doc::LiveShape::Ellipse { pie, .. } => { + widgets::subheader(ui, tl!("Ellipse Properties:")); + pie_rows(app, ui, *pie, "xfp-pie"); + } _ => { widgets::subheader(ui, tl!("Shape Properties:")); widgets::dim_label(ui, tl!(n.kind_label())); @@ -211,6 +215,22 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } +/// An ellipse's Pie Start and End Angle (degrees, counterclockwise from 3 o'clock; 0 to 360 is the +/// whole ellipse) and Invert Pie, which shows the other part of it (Transform and Properties panels). +pub(crate) fn pie_rows(app: &mut VectorcraftApp, ui: &mut Ui, pie: (f64, f64), id: &str) { + for (label, key, v) in [(tl!("Pie Start Angle:"), "pieStart", pie.0), (tl!("Pie End Angle:"), "pieEnd", pie.1)] { + ui.horizontal(|ui| { + widgets::dim_label(ui, label); + if let Some(a) = widgets::plain_field(ui, (id, key), v, "°", 1, 60.0) { + app.run("object.setLiveShape", json!({ key: a })).ok(); + } + }); + } + if widgets::flat_button(ui, tl!("Invert Pie"), 90.0).clicked() { + app.run("object.setLiveShape", json!({"invertPie": true})).ok(); + } +} + /// The Control bar's underlined "Transform" link: a click toggles the Transform panel in a /// popover under it, as the Stroke link does with the Stroke panel. pub fn link(app: &mut VectorcraftApp, ui: &mut Ui) {