diff --git a/ROADMAP.md b/ROADMAP.md index e9a65e961..18cd83ddd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,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-13** · Text Frame Options is laid out as InDesign's: General (Columns: Fixed Number or Fixed Width with Number, Width and Gutter; Balance Columns; Inset Spacing with the link button; Vertical Justification; Ignore Text Wrap), Baseline Options (First Baseline Offset and Min) and Column Rules. `object.textFrameOptions` takes `columnWidth` and `firstBaselineMin`. No readiness percentage change. - **2026-10-13** · The Window menu follows InDesign's order (Arrange, Workspace, then the panels alphabetically with Color, Editorial, Object & Layout, Styles… submenus), and Window › Workspace lists the workspaces as InDesign does, now with Essentials Classic: the Control panel and an icon column of named panel groups. No readiness percentage change. - **2026-10-13** · `.indd` and `.indt` documents open through File › Open, drop, the web inbox, `file.open`, `file.openBytes`, `file.openIdml` and the CLI: [storytold/indd-utils](https://github.com/storytold/indd-utils) converts them to IDML in memory and the IDML importer reads the result, on desktop and web. Converter warnings come first in the open result; an unreadable file is an open error (#21, #114). No readiness percentage change until a real-file corpus measures it. diff --git a/crates/doc/src/item.rs b/crates/doc/src/item.rs index d0efbc1df..68c9172e4 100644 --- a/crates/doc/src/item.rs +++ b/crates/doc/src/item.rs @@ -991,6 +991,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")] @@ -1007,6 +1010,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)] @@ -1028,11 +1064,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 16036f279..eed77a94b 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -525,7 +525,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, effects_all ), @@ -2035,6 +2035,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)?; } @@ -5000,3 +5001,27 @@ mod fixed_width_tests { assert!((s.doc().unwrap().doc.item(id).unwrap().inner_bounds().width() - 252.0).abs() < 1e-6); } } + +#[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 1390220e7..5f50346c2 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -136,10 +136,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); @@ -147,14 +154,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(); @@ -216,13 +223,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); @@ -238,10 +248,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. @@ -312,13 +322,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. @@ -337,11 +349,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 51871ae48..ed0beadf6 100644 --- a/crates/render/tests/effects.rs +++ b/crates/render/tests/effects.rs @@ -272,3 +272,44 @@ fn effects_apply_to_a_frame_with_pasted_in_items() { let img = render(&d); assert!(lum(img.pixel(136, 80)) < 300, "the shadow to the right: {:?}", img.pixel(136, 80)); } + +#[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 e9f082fe1..0036a37f6 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() @@ -342,6 +343,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:"); @@ -359,6 +377,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" }); @@ -433,6 +464,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 101fd6b1c..37297d8c4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -74,6 +74,12 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Vertical Justification", ["Vertikale Ausrichtung", "Justification verticale", "Justificación vertical", "垂直比率", "垂直对齐"]), ("Align:", ["Ausrichten:", "Alignement :", "Alinear:", "整列:", "对齐:"]), ("Ignore Text Wrap", ["Textumfluss ignorieren", "Ignorer l’habillage", "Ignorar ceñir texto", "テキストの回り込みを無視", "忽略文本绕排"]), + ("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", "シャープ", "锐化"]), + ("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 fd22f58a4..4f1f667e1 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -40,6 +40,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical Justification", "الضبط العمودي"), ("Align:", "المحاذاة:"), ("Ignore Text Wrap", "تجاهل التفاف النص"), + ("Technique:", "التقنية:"), + ("Softer", "أنعم"), + ("Precise", "دقيق"), + ("Corners:", "الزوايا:"), + ("Sharp", "حادة"), + ("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 cd146ab0e..d24e2f25e 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -38,6 +38,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical Justification", "Giustificazione verticale"), ("Align:", "Allinea:"), ("Ignore Text Wrap", "Ignora contorna con testo"), + ("Technique:", "Tecnica:"), + ("Softer", "Più morbido"), + ("Precise", "Preciso"), + ("Corners:", "Angoli:"), + ("Sharp", "Netti"), + ("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 10dd28d1e..95847a5a5 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -42,6 +42,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical Justification", "垂直比率"), ("Align:", "整列:"), ("Ignore Text Wrap", "テキストの回り込みを無視"), + ("Technique:", "テクニック:"), + ("Softer", "さらにソフトに"), + ("Precise", "精細"), + ("Corners:", "角:"), + ("Sharp", "シャープ"), + ("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 e3f23b7a9..a797cd6fd 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -41,6 +41,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical Justification", "Justificação vertical"), ("Align:", "Alinhar:"), ("Ignore Text Wrap", "Ignorar disposição do texto"), + ("Technique:", "Técnica:"), + ("Softer", "Mais suave"), + ("Precise", "Preciso"), + ("Corners:", "Cantos:"), + ("Sharp", "Nítidos"), + ("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 2291e67cd..746db2996 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -39,6 +39,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Vertical Justification", "Вертикальне вирівнювання"), ("Align:", "Вирівняти:"), ("Ignore Text Wrap", "Ігнорувати обтікання текстом"), + ("Technique:", "Техніка:"), + ("Softer", "М'якше"), + ("Precise", "Точно"), + ("Corners:", "Кути:"), + ("Sharp", "Гострі"), + ("Diffused", "Розсіяні"), ("Reverse Path", "Розвернути контур"), ("Set Anchor", "Задати прив’язку"), ("Effects…", "Ефекти…"),