diff --git a/ROADMAP.md b/ROADMAP.md index 2cdc1f32c..fc221baa3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-13** · Inner Glow takes a blend mode (Screen by default) and a Technique (Softer or Precise); Basic Feather takes Choke and Corners (Sharp, Rounded, Diffused), in the Effects dialog and `object.effects`. Inner glow and bevel blend modes now mix with the object below them instead of an empty layer. Noise isn't drawn. No readiness percentage change. - **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change. - **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change. - **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change. diff --git a/crates/doc/src/item.rs b/crates/doc/src/item.rs index 10da62c07..cd6dcf5d8 100644 --- a/crates/doc/src/item.rs +++ b/crates/doc/src/item.rs @@ -777,6 +777,9 @@ pub struct Effects { pub drop_shadow: DropShadow, /// Basic feather width (0 = off). pub feather: f64, + /// Basic feather's choke and corners. + #[serde(skip_serializing_if = "is_default")] + pub feather_options: FeatherOptions, #[serde(skip_serializing_if = "is_default")] pub inner_shadow: InnerShadow, #[serde(skip_serializing_if = "is_default")] @@ -793,6 +796,39 @@ pub struct Effects { pub directional_feather: DirectionalFeather, } +/// Basic Feather › Corners: how the fade follows the shape's corners. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum FeatherCorners { + /// Mitred: the fade keeps the corners sharp. + Sharp, + /// The fade rounds the corners by its width. + Rounded, + /// From opaque to transparent, softly (InDesign's default). + #[default] + Diffused, +} + +/// Basic Feather options besides its width. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct FeatherOptions { + /// Percentage (0–100) of the width that stays opaque: the fade gets shorter. + pub choke: f64, + pub corners: FeatherCorners, +} + +/// Inner/Outer Glow › Technique. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum GlowTechnique { + /// A soft, blurred glow. + #[default] + Softer, + /// A glow that keeps closer to the shape's edges. + Precise, +} + /// Directional Feather: each side fades over its own width (item space, before rotation). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] @@ -814,11 +850,23 @@ pub struct InnerGlow { pub choke: f64, /// Glow from the centre instead of the edges. pub center: bool, + /// How the glow blends with the object (InDesign: Screen). + pub mode: BlendMode, + pub technique: GlowTechnique, } impl Default for InnerGlow { fn default() -> Self { - InnerGlow { on: false, color: designcraft_color::swatch::PAPER.into(), opacity: 0.75, size: 7.0, choke: 0.0, center: false } + InnerGlow { + on: false, + color: designcraft_color::swatch::PAPER.into(), + opacity: 0.75, + size: 7.0, + choke: 0.0, + center: false, + mode: BlendMode::Screen, + technique: GlowTechnique::Softer, + } } } diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index de642a042..95c479844 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -527,7 +527,7 @@ pub fn specs() -> Vec { "Effects", [], None, - "{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, feather? (width), directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step", + "{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow? {on?, color?, opacity?, size?, choke?, center?, mode? (blend mode: Screen, Multiply, …; Screen by default), technique?: softer|precise}, bevel?, satin?, feather? (width), featherOptions? {choke? (%), corners?: sharp|rounded|diffused}, directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step", has_selection, set_effects ), @@ -1553,6 +1553,7 @@ fn sanitize_effects(e: &mut designcraft_doc::Effects) -> Result<()> { len(&mut sa.distance)?; len(&mut sa.size)?; len(&mut e.feather)?; + pct(&mut e.feather_options.choke)?; for w in &mut e.directional_feather.widths { len(w)?; } @@ -3508,3 +3509,27 @@ mod effects_command_tests { assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc)); } } + +#[cfg(test)] +mod glow_feather_options_tests { + use serde_json::json; + + #[test] + fn glow_mode_technique_and_feather_options_are_kept() { + let mut s = crate::Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = s.execute("frame.create", &json!({"rect": [0, 0, 50, 50]})).unwrap()["id"].as_u64().unwrap(); + s.execute( + "object.effects", + &json!({"ids": [id], "effects": {"innerGlow": {"on": true, "mode": "Multiply", "technique": "precise"}, "feather": 4.5, "featherOptions": {"choke": 140, "corners": "sharp"}}}), + ) + .unwrap(); + let e = s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().effects.clone(); + assert_eq!((e.inner_glow.mode, e.inner_glow.technique), (designcraft_color::BlendMode::Multiply, designcraft_doc::GlowTechnique::Precise)); + assert_eq!((e.feather_options.choke, e.feather_options.corners), (100.0, designcraft_doc::FeatherCorners::Sharp), "choke is a percentage"); + // InDesign's defaults: Screen and Softer; Diffused corners, no choke. + let d = designcraft_doc::Effects::default(); + assert_eq!((d.inner_glow.mode, d.inner_glow.technique), (designcraft_color::BlendMode::Screen, designcraft_doc::GlowTechnique::Softer)); + assert_eq!((d.feather_options.choke, d.feather_options.corners), (0.0, designcraft_doc::FeatherCorners::Diffused)); + } +} diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 6b29516f4..f6946e8b9 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -113,10 +113,17 @@ impl Renderer { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, None, None, None, None); } - // The object, feathered or not. + // The object, feathered or not. Choke keeps that much of the width opaque (a shorter + // fade); Sharp and Rounded corners follow the inset shape more tightly than Diffused. if e.feather > 0.0 && it.path.is_closed() { - let w = e.feather; - self.with_filters(ctx, f, reach, w / 4.0, |me, c, fr| { + let fo = &e.feather_options; + let w = e.feather * (1.0 - fo.choke.clamp(0.0, 100.0) / 100.0).max(0.01); + let (sigma, join) = match fo.corners { + designcraft_doc::FeatherCorners::Diffused => (w / 4.0, kurbo::Join::Round), + designcraft_doc::FeatherCorners::Rounded => (w / 3.0, kurbo::Join::Round), + designcraft_doc::FeatherCorners::Sharp => (w / 6.0, kurbo::Join::Miter), + }; + self.with_filters(ctx, f, reach, sigma, |me, c, fr| { c.set_transform(fr.view * xf); c.push_clip_layer(bp); me.draw_body(c, fr, it, bp, xf, page); @@ -124,14 +131,14 @@ impl Renderer { c.set_transform(Affine::IDENTITY); c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None); c.set_transform(fr.view); - c.push_layer(None, None, None, None, Some(blur(w / 4.0))); + c.push_layer(None, None, None, None, Some(blur(sigma))); c.set_transform(fr.view * xf); c.set_paint(peniko::Color::BLACK); c.fill_path(bp); c.set_transform(Affine::IDENTITY); c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None); c.set_transform(fr.view * xf); - c.set_stroke(kurbo::Stroke::new(w).with_join(kurbo::Join::Round)); + c.set_stroke(kurbo::Stroke::new(w).with_join(join)); c.stroke_path(bp); c.pop_layer(); c.pop_layer(); @@ -208,13 +215,16 @@ impl Renderer { { let g = &e.inner_glow; let (sigma, choke) = split(g.size, g.choke); + // Precise keeps the glow close to the edge: a tighter blur. + let sigma = if g.technique == designcraft_doc::GlowTechnique::Precise { sigma * 0.4 } else { sigma }; + let mix = crate::blend_mode(g.mode).mix; let paint = color_of(&c, g.opacity.clamp(0.0, 1.0)); if g.center { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None); self.with_filters(ctx, f, reach, sigma, |_, c, fr| { c.set_transform(fr.view * xf); c.push_clip_layer(bp); - c.set_transform(Affine::IDENTITY); - c.push_layer(None, Some(BlendMode::new(Mix::Screen, Compose::SrcOver)), None, None, None); c.set_transform(fr.view); c.push_layer(None, None, None, None, Some(blur(sigma))); c.set_transform(fr.view * xf); @@ -230,10 +240,10 @@ impl Renderer { c.pop_layer(); c.pop_layer(); c.pop_layer(); - c.pop_layer(); }); + ctx.pop_layer(); } else { - self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, Mix::Screen); + self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, mix); } } // Satin: the shape shifted both ways along the angle, blurred, where exactly one covers. @@ -304,13 +314,15 @@ impl Renderer { paint: peniko::Color, mix: Mix, ) { + // The blend mode applies to the finished (clipped, blurred) edge against what's below it: + // inside the clip layer it would only meet that layer's transparent backdrop. + if mix != Mix::Normal { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None); + } self.with_filters(ctx, f, reach, sigma, |_, c, fr| { c.set_transform(fr.view * xf); c.push_clip_layer(bp); - if mix != Mix::Normal { - c.set_transform(Affine::IDENTITY); - c.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None); - } c.set_transform(fr.view); c.push_layer(None, None, None, None, Some(blur(sigma))); // Everything outside the offset shape, so the colour bleeds in from the edges. @@ -329,11 +341,11 @@ impl Renderer { c.stroke_path(&shape); } c.pop_layer(); - if mix != Mix::Normal { - c.pop_layer(); - } c.pop_layer(); }); + if mix != Mix::Normal { + ctx.pop_layer(); + } } /// Run `draw`, which pushes filter layers. Single-threaded contexts draw directly. On a diff --git a/crates/render/tests/effects.rs b/crates/render/tests/effects.rs index 35c0c0896..9c64d7662 100644 --- a/crates/render/tests/effects.rs +++ b/crates/render/tests/effects.rs @@ -224,3 +224,44 @@ fn multithreaded_effects_match_single_threaded() { assert!(worst <= 4, "case {i}: max channel difference {worst}"); } } + +#[test] +fn inner_glow_mode_and_technique() { + let mut d = doc(); + let id = rect_item(&mut d, Rect::new(30.0, 30.0, 130.0, 130.0), "[Paper]"); + let g = &mut d.item_mut(id).unwrap().effects.inner_glow; + g.on = true; + g.color = "[Black]".into(); + g.opacity = 1.0; + g.size = 20.0; + // Screen (the default) can't darken white paper; Multiply can. + let screen = render(&d); + assert!(lum(screen.pixel(32, 80)) > 740, "{:?}", screen.pixel(32, 80)); + d.item_mut(id).unwrap().effects.inner_glow.mode = designcraft_color::BlendMode::Multiply; + let soft = render(&d); + assert!(lum(soft.pixel(32, 80)) + 150 < lum(screen.pixel(32, 80)), "a dark edge: {:?}", soft.pixel(32, 80)); + // Precise keeps the glow nearer the edge than Softer. + d.item_mut(id).unwrap().effects.inner_glow.technique = designcraft_doc::GlowTechnique::Precise; + let precise = render(&d); + assert!(lum(precise.pixel(42, 80)) > lum(soft.pixel(42, 80)) + 30, "{:?} vs {:?}", precise.pixel(42, 80), soft.pixel(42, 80)); +} + +#[test] +fn feather_choke_and_corners() { + let mut d = doc(); + let id = rect_item(&mut d, Rect::new(30.0, 30.0, 130.0, 130.0), "[Black]"); + d.item_mut(id).unwrap().effects.feather = 16.0; + let diffused = render(&d); + golden("feather", &diffused); + // Choke: half the width stays opaque, so the fade is shorter. + d.item_mut(id).unwrap().effects.feather_options.choke = 50.0; + let choked = render(&d); + assert!(lum(choked.pixel(38, 80)) + 60 < lum(diffused.pixel(38, 80)), "{:?} vs {:?}", choked.pixel(38, 80), diffused.pixel(38, 80)); + assert!(lum(choked.pixel(30, 80)) > 500, "still transparent at the path"); + // Sharp corners keep more of the corner than Rounded ones (past the fade, near the corner). + d.item_mut(id).unwrap().effects.feather_options = designcraft_doc::FeatherOptions { choke: 0.0, corners: designcraft_doc::FeatherCorners::Sharp }; + let sharp = render(&d); + d.item_mut(id).unwrap().effects.feather_options.corners = designcraft_doc::FeatherCorners::Rounded; + let rounded = render(&d); + assert!(lum(sharp.pixel(41, 41)) + 20 < lum(rounded.pixel(41, 41)), "{:?} vs {:?}", sharp.pixel(41, 41), rounded.pixel(41, 41)); +} diff --git a/crates/ui-egui/src/effects_dialog.rs b/crates/ui-egui/src/effects_dialog.rs index 9a0e98620..6211a8f15 100644 --- a/crates/ui-egui/src/effects_dialog.rs +++ b/crates/ui-egui/src/effects_dialog.rs @@ -66,7 +66,7 @@ pub fn open(app: &mut DesignApp, page: &str) { /// Every effect's settings, those still at their defaults included (the document leaves them out). fn effects_json(e: &designcraft_doc::Effects) -> Value { - use designcraft_doc::{Bevel, DirectionalFeather, DropShadow, GradientFeather, InnerGlow, InnerShadow, OuterGlow, Satin}; + use designcraft_doc::{Bevel, DirectionalFeather, DropShadow, FeatherOptions, GradientFeather, InnerGlow, InnerShadow, OuterGlow, Satin}; let mut fx = serde_json::to_value(e).unwrap_or_else(|_| json!({})); let defaults = [ ("dropShadow", serde_json::to_value(DropShadow::default())), @@ -77,6 +77,7 @@ fn effects_json(e: &designcraft_doc::Effects) -> Value { ("satin", serde_json::to_value(Satin::default())), ("directionalFeather", serde_json::to_value(DirectionalFeather::default())), ("gradientFeather", serde_json::to_value(GradientFeather::default())), + ("featherOptions", serde_json::to_value(FeatherOptions::default())), ]; for (k, v) in defaults { if !fx[k].is_object() @@ -351,6 +352,23 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) { let key = page; heading(ui, &tr("Blending")); grid("b").show(ui, |ui| { + // Inner Glow: its blend mode (Screen, as InDesign starts it). + if key == "innerGlow" { + row(ui, "Mode:"); + let cur = d.fields.get("fx").and_then(|f| f["innerGlow"]["mode"].as_str()).unwrap_or("Screen").to_string(); + let label = |m: &designcraft_color::BlendMode| { + serde_json::to_value(m).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default() + }; + let shown = designcraft_color::BlendMode::ALL.iter().find(|m| label(m) == cur).map_or("Screen", |m| m.label()); + egui::ComboBox::from_id_salt("fx_glow_mode").selected_text(tr(shown)).width(150.0).show_ui(ui, |ui| { + for m in designcraft_color::BlendMode::ALL { + if ui.selectable_label(label(&m) == cur, tr(m.label())).clicked() { + set(d, &["innerGlow", "mode"], json!(label(&m))); + } + } + }); + ui.end_row(); + } row(ui, "Color:"); swatch(app, ui, d, &[key, "color"]); row(ui, "Opacity:"); @@ -368,6 +386,19 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) { row(ui, "Choke:"); percent(ui, d, &[key, "choke"]); ui.end_row(); + row(ui, "Technique:"); + let cur = d.fields.get("fx").and_then(|f| f["innerGlow"]["technique"].as_str()).unwrap_or("softer").to_string(); + egui::ComboBox::from_id_salt("fx_technique").selected_text(tr(if cur == "precise" { "Precise" } else { "Softer" })).show_ui( + ui, + |ui| { + for (v, l) in [("softer", "Softer"), ("precise", "Precise")] { + if ui.selectable_label(cur == v, tr(l)).clicked() { + set(d, &["innerGlow", "technique"], json!(v)); + } + } + }, + ); + ui.end_row(); row(ui, "Source:"); let mut center = d.fields.get("fx").is_some_and(|f| f["innerGlow"]["center"].as_bool().unwrap_or(false)); let label = |c: bool| tr(if c { "Center" } else { "Edge" }); @@ -442,6 +473,24 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) { row(ui, "Feather Width:"); measure(ui, d, &["feather"], unit); ui.end_row(); + row(ui, "Choke:"); + percent(ui, d, &["featherOptions", "choke"]); + ui.end_row(); + row(ui, "Corners:"); + let cur = d.fields.get("fx").and_then(|f| f["featherOptions"]["corners"].as_str()).unwrap_or("diffused").to_string(); + let shown = match cur.as_str() { + "sharp" => "Sharp", + "rounded" => "Rounded", + _ => "Diffused", + }; + egui::ComboBox::from_id_salt("fx_corners").selected_text(tr(shown)).show_ui(ui, |ui| { + for (v, l) in [("sharp", "Sharp"), ("rounded", "Rounded"), ("diffused", "Diffused")] { + if ui.selectable_label(cur == v, tr(l)).clicked() { + set(d, &["featherOptions", "corners"], json!(v)); + } + } + }); + ui.end_row(); }); } "directionalFeather" => { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index ae5308a0e..73ed1ca56 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -55,6 +55,13 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), + ("Technique:", ["Technik:", "Technique :", "Técnica:", "テクニック:", "方法:"]), + ("Softer", ["Weicher", "Plus flou", "Más suave", "さらにソフトに", "柔和"]), + ("Precise", ["Präzise", "Précis", "Precisa", "精細", "精确"]), + ("Corners:", ["Ecken:", "Angles :", "Esquinas:", "角:", "角点:"]), + ("Sharp", ["Spitz", "Vifs", "Nítidas", "シャープ", "锐化"]), + ("Rounded", ["Abgerundet", "Arrondis", "Redondeadas", "角丸", "圆角"]), + ("Diffused", ["Weich", "Diffus", "Difusas", "拡散", "扩散"]), ("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]), ("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]), ("Effects…", ["Effekte…", "Effets…", "Efectos…", "効果…", "效果…"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 33e9ea9fd..94e33d168 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Technique:", "التقنية:"), + ("Softer", "أنعم"), + ("Precise", "دقيق"), + ("Corners:", "الزوايا:"), + ("Sharp", "حادة"), + ("Rounded", "مستديرة"), + ("Diffused", "منتشرة"), ("Reverse Path", "عكس المسار"), ("Set Anchor", "تعيين نقطة الارتساء"), ("Effects…", "التأثيرات…"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index cd90efd11..2e10b7de6 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Technique:", "Tecnica:"), + ("Softer", "Più morbido"), + ("Precise", "Preciso"), + ("Corners:", "Angoli:"), + ("Sharp", "Netti"), + ("Rounded", "Arrotondati"), + ("Diffused", "Diffusi"), ("Reverse Path", "Inverti tracciato"), ("Set Anchor", "Imposta ancoraggio"), ("Effects…", "Effetti…"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 50b1d88af..6d8f43094 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Technique:", "テクニック:"), + ("Softer", "さらにソフトに"), + ("Precise", "精細"), + ("Corners:", "角:"), + ("Sharp", "シャープ"), + ("Rounded", "角丸"), + ("Diffused", "拡散"), ("Reverse Path", "パスの方向反転"), ("Set Anchor", "アンカーポイントを設定"), ("Effects…", "効果…"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 6476421d9..a73e09779 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Technique:", "Técnica:"), + ("Softer", "Mais suave"), + ("Precise", "Preciso"), + ("Corners:", "Cantos:"), + ("Sharp", "Nítidos"), + ("Rounded", "Arredondados"), + ("Diffused", "Difusos"), ("Reverse Path", "Inverter demarcador"), ("Set Anchor", "Definir ponto de ancoragem"), ("Effects…", "Efeitos…"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index d44fc194b..54f1eaf0c 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Technique:", "Техніка:"), + ("Softer", "М'якше"), + ("Precise", "Точно"), + ("Corners:", "Кути:"), + ("Sharp", "Гострі"), + ("Rounded", "Заокруглені"), + ("Diffused", "Розсіяні"), ("Reverse Path", "Розвернути контур"), ("Set Anchor", "Задати прив’язку"), ("Effects…", "Ефекти…"),