From c8167c54eef8be243f0228f815089e9fee7411da Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:50:00 +0300 Subject: [PATCH 1/7] -VIEW sEttings Background dialog and view backgrounds Color, Gradient and Image at the background prompt open a Background dialog (None / Solid / Gradient / Image, preview, solid colour, two- or three-colour gradient with rotation, image file with position and scale). Apply links the view to a new background object in ACAD_BACKGROUND; restoring a view gives the model viewport a copy of it (or none), and saving a view copies the viewport's background. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 32 ++++ locales/bg-BG/opencadstudio.ftl | 32 ++++ locales/cs-CZ/opencadstudio.ftl | 32 ++++ locales/de-DE/opencadstudio.ftl | 32 ++++ locales/el-GR/opencadstudio.ftl | 32 ++++ locales/en-US/opencadstudio.ftl | 32 ++++ locales/es-ES/opencadstudio.ftl | 32 ++++ locales/fi-FI/opencadstudio.ftl | 32 ++++ locales/fr-FR/opencadstudio.ftl | 32 ++++ locales/hi-IN/opencadstudio.ftl | 32 ++++ locales/hu-HU/opencadstudio.ftl | 32 ++++ locales/it-IT/opencadstudio.ftl | 32 ++++ locales/ja-JP/opencadstudio.ftl | 32 ++++ locales/ko-KR/opencadstudio.ftl | 32 ++++ locales/lo-LA/opencadstudio.ftl | 32 ++++ locales/nl-NL/opencadstudio.ftl | 32 ++++ locales/pl-PL/opencadstudio.ftl | 32 ++++ locales/pt-BR/opencadstudio.ftl | 32 ++++ locales/ru-RU/opencadstudio.ftl | 32 ++++ locales/tr-TR/opencadstudio.ftl | 32 ++++ locales/zh-CN/opencadstudio.ftl | 32 ++++ locales/zh-TW/opencadstudio.ftl | 32 ++++ src/app/commands/pdf_dialogs.rs | 11 ++ src/app/commands/view_presets.rs | 208 +++++++++++++++++++++- src/app/control/actions.rs | 48 +++++ src/app/mod.rs | 6 + src/app/update/mod.rs | 7 + src/app/update/style.rs | 12 ++ src/app/view/modal.rs | 11 ++ src/modules/view/view_cmd.rs | 11 +- src/scene/camera_ops.rs | 86 +++++++++ src/scene/mod.rs | 1 + src/scene/view/render.rs | 5 + src/ui/window/pdf_dialogs.rs | 294 +++++++++++++++++++++++++++++++ 34 files changed, 1400 insertions(+), 4 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index a13a13223..2b7667ca3 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6220,3 +6220,35 @@ graphic-attributes = .reset-angle = إعادة تعيين الزاوية .one-color = لون واحد .two-colors = لونان + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index d6d751979..6301e8d1f 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6208,3 +6208,35 @@ graphic-attributes = .reset-angle = Нулиране на ъгъла .one-color = Един цвят .two-colors = Два цвята + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 779f63fb7..7863c71aa 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6205,3 +6205,35 @@ graphic-attributes = .reset-angle = Obnovit úhel .one-color = Jedna barva .two-colors = Dvě barvy + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index f0e33e76b..a56f9511b 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6206,3 +6206,35 @@ graphic-attributes = .reset-angle = Winkel zurücksetzen .one-color = Einfarbig .two-colors = Zweifarbig + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index d3906e20d..a75508004 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6208,3 +6208,35 @@ graphic-attributes = .reset-angle = Επαναφορά γωνίας .one-color = Ένα χρώμα .two-colors = Δύο χρώματα + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 8ae79dd11..a3389377c 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6209,3 +6209,35 @@ graphic-attributes = .reset-angle = Reset angle .one-color = One Color .two-colors = Two Colors + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 282915ac7..d332786fa 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6208,3 +6208,35 @@ graphic-attributes = .reset-angle = Restablecer ángulo .one-color = Un color .two-colors = Dos colores + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 71e626c9e..5e77c4ebc 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6205,3 +6205,35 @@ graphic-attributes = .reset-angle = Palauta kulma .one-color = Yksi väri .two-colors = Kaksi väriä + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 47ab852c5..8ba0b5ee8 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6207,3 +6207,35 @@ graphic-attributes = .reset-angle = Réinitialiser l'angle .one-color = Une couleur .two-colors = Deux couleurs + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index b6e4ed947..5aaffbdc6 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6196,3 +6196,35 @@ graphic-attributes = .reset-angle = कोण रीसेट करें .one-color = एक रंग .two-colors = दो रंग + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 00674b1f2..8082c7a92 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6194,3 +6194,35 @@ graphic-attributes = .reset-angle = Szög visszaállítása .one-color = Egy szín .two-colors = Két szín + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index d84e500ad..e04a56056 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6207,3 +6207,35 @@ graphic-attributes = .reset-angle = Reimposta angolo .one-color = Un colore .two-colors = Due colori + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index ed37d6a97..70e632c1e 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6196,3 +6196,35 @@ graphic-attributes = .reset-angle = 角度をリセット .one-color = 1 色 .two-colors = 2 色 + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index a5ed580ee..8b3a260d0 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6207,3 +6207,35 @@ graphic-attributes = .reset-angle = 각도 재설정 .one-color = 단색 .two-colors = 두 색 + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 7304c8344..c26755152 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -6209,3 +6209,35 @@ graphic-attributes = .reset-angle = ຣີເຊັດມຸມ .one-color = ໜຶ່ງສີ .two-colors = ສອງສີ + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 4ad871607..195ff3b44 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6206,3 +6206,35 @@ graphic-attributes = .reset-angle = Hoek herstellen .one-color = Eén kleur .two-colors = Twee kleuren + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 7ba65e368..0a33d5796 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6207,3 +6207,35 @@ graphic-attributes = .reset-angle = Resetuj kąt .one-color = Jeden kolor .two-colors = Dwa kolory + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 2e21eeeef..8601be839 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6208,3 +6208,35 @@ graphic-attributes = .reset-angle = Redefinir ângulo .one-color = Uma cor .two-colors = Duas cores + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 9672928b6..e03bdb029 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6209,3 +6209,35 @@ graphic-attributes = .reset-angle = Сбросить угол .one-color = Один цвет .two-colors = Два цвета + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 31ed14f3f..34ca01758 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6131,3 +6131,35 @@ graphic-attributes = .reset-angle = Açıyı sıfırla .one-color = Tek renk .two-colors = İki renk + +view-background = + .title = Arka Plan + .none = Yok + .solid = Düz + .gradient = Gradyan + .image = Görüntü + .preview = Önizleme + .solid-color = Düz renk + .color = Renk + .select = Seç… + .three-colors = Üç renk + .rotation = Döndürme + .top = Üst renk + .middle = Orta renk + .bottom = Alt renk + .browse = Gözat… + .position = Yerleşim + .center = Ortala + .stretch = Uzat + .tile = Döşe + .scale = Ölçek + .fit = Ekrana sığdır + .aspect = Oranı koru + .no-image = görüntü seçilmedi + .canvas = çizim alanı rengi + .info = Bilgi + .none-info = Görünüm geri yüklendiğinde arka plan değişmez; çizim alanı rengi kullanılır. + .apply = Uygula + .help = Adlandırılmış görünüm geri yüklendiğinde gösterilecek arka planı ayarlar. + .image-files = Görüntü dosyaları + .select-image = Arka Plan Görüntüsü Seç diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 521e92320..0948800ad 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6191,3 +6191,35 @@ graphic-attributes = .reset-angle = 重置角度 .one-color = 单色 .two-colors = 双色 + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 3346a8f09..139e45847 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6203,3 +6203,35 @@ graphic-attributes = .reset-angle = 重設角度 .one-color = 單色 .two-colors = 雙色 + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Select… + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browse… + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .fit = Fit to screen + .aspect = Keep aspect ratio + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index ec69e6e12..2c14389c8 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -439,6 +439,7 @@ impl OpenCADStudio { "attach" => crate::t!("Attaches a PDF file as an underlay."), "pointcloud" => crate::t!("Attaches a point cloud scan or project."), "pcsection" => crate::t!("Traces the points on the kept side of a live section as lines."), + "background" => std::borrow::Cow::Owned(crate::tr!("view-background", "help")), "pointcloudcolormap" => { crate::t!("Sets the color schemes and ranges a point cloud is colored by intensity or elevation.") } @@ -572,6 +573,16 @@ impl OpenCADStudio { | PdfDialogMsg::MapCurrent(_) | PdfDialogMsg::MapApply | PdfDialogMsg::MapOk) => return self.update_point_cloud_color_map(message), + message @ (PdfDialogMsg::BgKind(_) + | PdfDialogMsg::BgPickColor(_) + | PdfDialogMsg::BgColor(..) + | PdfDialogMsg::BgThree(_) + | PdfDialogMsg::BgRotation(_) + | PdfDialogMsg::BgImage(_) + | PdfDialogMsg::BgBrowse + | PdfDialogMsg::BgPosition(_) + | PdfDialogMsg::BgAspect(_) + | PdfDialogMsg::BgApply) => return self.update_view_background(message), message @ (PdfDialogMsg::SecPerimeter(_) | PdfDialogMsg::SecMaxPoints(_) | PdfDialogMsg::SecLayer(_) diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 66dc10bb9..901deddd1 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -111,7 +111,7 @@ impl OpenCADStudio { } /// Carry out what -VIEW gathered (`-VIEW >VERB args`). - pub(super) fn run_dash_view(&mut self, rest: &str, i: usize) -> Task { + pub(crate) fn run_dash_view(&mut self, rest: &str, i: usize) -> Task { let (verb, arg) = rest.split_once(' ').unwrap_or((rest, "")); let mut settings_again = false; match verb { @@ -197,6 +197,14 @@ impl OpenCADStudio { } } "SETTINGS" => settings_again = true, + // Background type chosen: its dialog takes over until Apply or Cancel. + "BACKGROUND" => { + if let Some((view, kind)) = arg.split_once('\u{1f}') { + self.open_view_background(i, view, kind); + return self.finish_dispatch("-VIEW"); + } + settings_again = true; + } "UCSVIEW" => { view_cmd::set_ucs_view(arg == "1"); self.save_config(); @@ -245,14 +253,20 @@ impl OpenCADStudio { /// of a view saved again under the same name. fn save_named_view(&mut self, i: usize, mut view: codec::tables::View) { self.push_undo_snapshot(i, "-VIEW"); + let background = self.tabs[i].scene.active_background(); let document = &mut self.tabs[i].scene.document; + view.background_handle = crate::scene::copy_background(document, background); match document.views.get(&view.name).cloned() { Some(old) => { view.name = old.name; view.handle = old.handle; view.visual_style_handle = old.visual_style_handle; view.live_section_handle = old.live_section_handle; - view.background_handle = old.background_handle; + // The view keeps its background unless the viewport has + // one, which the view then takes a copy of. + if view.background_handle.is_null() { + view.background_handle = old.background_handle; + } } None => { view.handle = document.allocate_handle(); @@ -339,3 +353,193 @@ impl OpenCADStudio { self.tabs[i].dirty = true; } } + +impl OpenCADStudio { + /// The Background dialog for a named view, on the type typed at the + /// prompt and filled from the view's background where it has one. + fn open_view_background(&mut self, i: usize, view: &str, kind: &str) { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, ViewBackgroundState}; + use codec::objects::ClassObjectData as D; + let unpack = |c: u32| [(c >> 16) as u8, (c >> 8) as u8, c as u8]; + let canvas = self.tabs[i].scene.bg_color; + let mut state = ViewBackgroundState { + view: view.to_string(), + kind: match kind { + "SOLID" => BgKind::Solid, + "GRADIENT" => BgKind::Gradient, + "IMAGE" => BgKind::Image, + _ => BgKind::None, + }, + // The reference's defaults. + solid: [128, 128, 255], + top: [204, 204, 204], + middle: [128, 128, 128], + bottom: [51, 51, 51], + three: false, + rotation: "0".into(), + image: String::new(), + position: BgPosition::Center, + aspect: false, + canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8), + }; + let document = &self.tabs[i].scene.document; + let current = document.views.get(view).map_or(Handle::NULL, |v| v.background_handle); + if let Some(ObjectType::ClassObject(object)) = document.objects.get(¤t) { + match &object.data { + D::SolidBackground(value) => state.solid = unpack(value.color), + D::GradientBackground(value) => { + (state.top, state.middle, state.bottom) = ( + unpack(value.color_top), + unpack(value.color_middle), + unpack(value.color_bottom), + ); + state.three = value.height > 0.0; + state.rotation = crate::ui::window::pdf_dialogs::short(value.rotation); + } + D::ImageBackground(value) => { + state.image = value.filename.clone(); + state.position = if value.use_tiling { + BgPosition::Tile + } else if value.fit_to_screen { + BgPosition::Stretch + } else { + BgPosition::Center + }; + state.aspect = value.maintain_aspect_ratio; + } + _ => {} + } + } + self.view_background = Some(state); + self.active_modal = Some(crate::app::ModalKind::ViewBackground); + } + + pub(in crate::app) fn update_view_background( + &mut self, + message: crate::ui::window::pdf_dialogs::PdfDialogMsg, + ) -> Task { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, PdfDialogMsg as M}; + use codec::objects::{ + ClassObjectData as D, GradientBackground, ImageBackground, SolidBackground, + }; + let i = self.active_tab; + let Some(state) = self.view_background.as_mut() else { + return Task::none(); + }; + match message { + M::BgKind(kind) => state.kind = kind, + M::BgThree(on) => state.three = on, + M::BgRotation(value) => state.rotation = value, + M::BgImage(value) => state.image = value, + M::BgPosition(value) => state.position = value, + M::BgAspect(on) => state.aspect = on, + M::BgColor(slot, rgb) => match slot { + 0 => state.solid = rgb, + 1 => state.top = rgb, + 2 => state.middle = rgb, + _ => state.bottom = rgb, + }, + M::BgPickColor(slot) => { + let [r, g, b] = match slot { + 0 => state.solid, + 1 => state.top, + 2 => state.middle, + _ => state.bottom, + }; + return Task::done(Message::OpenColorWindow( + crate::app::ColorPickTarget::ViewBackground(slot), + codec::types::Color::Rgb { r, g, b }, + )); + } + M::BgBrowse => { + return Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::tr!("view-background", "select-image")) + .add_filter( + crate::tr!("view-background", "image-files"), + &["png", "jpg", "jpeg", "bmp", "tif", "tiff", "gif", "tga"], + ) + .pick_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + }, + |path| match path { + Some(path) => Message::PdfDialog(M::BgImage(path.display().to_string())), + None => Message::Noop, + }, + ); + } + M::BgApply => { + // Colours are stored as true colours (flag byte 0xC2), as the + // reference stores them. + let pack = |c: [u8; 3]| { + 0xC200_0000 | (c[0] as u32) << 16 | (c[1] as u32) << 8 | c[2] as u32 + }; + let data = match state.kind { + BgKind::None => None, + BgKind::Solid => Some(D::SolidBackground(SolidBackground { + class_version: 1, + color: pack(state.solid), + })), + BgKind::Gradient => { + let Some(rotation) = state + .rotation + .trim() + .parse::() + .ok() + .filter(|v| v.is_finite()) + else { + self.command_line + .push_error(crate::t!("Requires a number.").as_ref()); + return Task::none(); + }; + Some(D::GradientBackground(GradientBackground { + class_version: 1, + color_top: pack(state.top), + color_middle: pack(state.middle), + color_bottom: pack(state.bottom), + horizon: 0.5, + // The reference marks a three-colour gradient by its + // middle band's height. + height: if state.three { 0.33 } else { 0.0 }, + rotation, + })) + } + BgKind::Image => { + if !std::path::Path::new(state.image.trim()).is_file() { + self.command_line.push_error("Invalid Background Image File!"); + return Task::none(); + } + Some(D::ImageBackground(ImageBackground { + class_version: 1, + filename: state.image.trim().to_string(), + fit_to_screen: state.position == BgPosition::Stretch, + maintain_aspect_ratio: state.aspect, + use_tiling: state.position == BgPosition::Tile, + offset: codec::types::Vector2::new(0.0, 0.0), + scale: codec::types::Vector2::new(1.0, 1.0), + })) + } + }; + let name = state.view.clone(); + self.view_background = None; + self.close_active_modal(); + if self.tabs[i].scene.document.views.get(&name).is_some() { + self.push_undo_snapshot(i, "-VIEW"); + let document = &mut self.tabs[i].scene.document; + let handle = data.map_or(Handle::NULL, |data| { + crate::scene::add_background(document, data) + }); + if let Some(view) = document.views.get_mut(&name) { + view.background_handle = handle; + } + self.tabs[i].dirty = true; + } + return self.run_dash_view("SETTINGS", i); + } + _ => {} + } + Task::none() + } +} diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index cbe3fdcb0..b2e0acb7f 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -103,6 +103,7 @@ pub(super) const NAMES: &[&str] = &[ #[cfg(not(target_arch = "wasm32"))] "external_change_cancel", "pdf_dialog_ok", + "view_background", "pdf_layer_toggle", "pc_manager_toggle", "pc_manager", @@ -1786,6 +1787,53 @@ impl OpenCADStudio { }) } "ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()), + // The -VIEW Background dialog, one field per call: "type=gradient", + // "color|top|middle|bottom=r,g,b", "three=1", "rotation=30", + // "image=", "position=center|stretch|tile", "aspect=1", + // "apply" or "cancel". + "view_background" => { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, PdfDialogMsg as P}; + if self.active_modal != Some(crate::app::ModalKind::ViewBackground) { + return Err(failure("no_dialog", "The Background dialog is not open")); + } + let spec = string(req, "value")?; + let (key, value) = spec.split_once('=').unwrap_or((spec, "")); + let bad = || failure("invalid_value", spec); + let rgb = || -> Result<[u8; 3], Value> { + let parts: Vec = value + .split(',') + .map(|part| part.trim().parse::()) + .collect::>() + .map_err(|_| bad())?; + <[u8; 3]>::try_from(parts).map_err(|_| bad()) + }; + match key { + "cancel" => Message::CloseModal, + "apply" => Message::PdfDialog(P::BgApply), + "type" => Message::PdfDialog(P::BgKind(match value { + "none" => BgKind::None, + "solid" => BgKind::Solid, + "gradient" => BgKind::Gradient, + "image" => BgKind::Image, + _ => return Err(bad()), + })), + "color" => Message::PdfDialog(P::BgColor(0, rgb()?)), + "top" => Message::PdfDialog(P::BgColor(1, rgb()?)), + "middle" => Message::PdfDialog(P::BgColor(2, rgb()?)), + "bottom" => Message::PdfDialog(P::BgColor(3, rgb()?)), + "three" => Message::PdfDialog(P::BgThree(value == "1")), + "rotation" => Message::PdfDialog(P::BgRotation(value.to_string())), + "image" => Message::PdfDialog(P::BgImage(value.to_string())), + "position" => Message::PdfDialog(P::BgPosition(match value { + "center" => BgPosition::Center, + "stretch" => BgPosition::Stretch, + "tile" => BgPosition::Tile, + _ => return Err(bad()), + })), + "aspect" => Message::PdfDialog(P::BgAspect(value == "1")), + _ => return Err(bad()), + } + } "pdf_dialog_ok" => { use crate::ui::window::pdf_dialogs::PdfDialogMsg; Message::PdfDialog(match self.active_modal { diff --git a/src/app/mod.rs b/src/app/mod.rs index 4462dd48a..99e714609 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -502,6 +502,8 @@ pub(super) struct OpenCADStudio { field_dialog: Option, point_cloud_color_map: Option, pc_section: Option, + /// -VIEW sEttings Background dialog. + view_background: Option, underlay_layers: Option, pdf_import_settings: Option, pdf_import_file: Option, @@ -1641,6 +1643,8 @@ pub enum ColorPickTarget { LayerState(usize), /// The MText editor's selection (or global) colour. MText, + /// A colour of the view Background dialog: 0 solid, 1 top, 2 middle, 3 bottom. + ViewBackground(u8), } /// Table records the clipboard entities depend on, snapshotted from the source @@ -1981,6 +1985,7 @@ pub enum ModalKind { PointCloudAttach, PointCloudColorMap, PcSection, + ViewBackground, UnderlayLayers, PdfImportSettings, PdfImportFile, @@ -4186,6 +4191,7 @@ impl OpenCADStudio { field_dialog: None, point_cloud_color_map: None, pc_section: None, + view_background: None, underlay_layers: None, pdf_import_settings: None, pdf_import_file: None, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 0ab8654a2..636841cac 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -8799,6 +8799,13 @@ impl OpenCADStudio { self.close_sheet_set_dialog(); return Task::none(); } + // The Background dialog returns to -VIEW's settings prompt. + if self.active_modal == Some(super::ModalKind::ViewBackground) { + self.view_background = None; + self.close_active_modal(); + let i = self.active_tab; + return self.run_dash_view("SETTINGS", i); + } if self.active_modal == Some(super::ModalKind::Field) { self.close_field_dialog(); return Task::none(); diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 2deac74ec..8ffb8f908 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -1162,6 +1162,18 @@ impl OpenCADStudio { crate::ui::window::gradient_editor::GradientMsg::Color(index, color), ))) } + Some(crate::app::ColorPickTarget::ViewBackground(slot)) => { + let rgb = match color { + codec::types::Color::Rgb { r, g, b } => Some([r, g, b]), + codec::types::Color::Index(index) => { + codec::types::aci_table::aci_to_rgb(index).map(|(r, g, b)| [r, g, b]) + } + _ => None, + }; + rgb.map(|rgb| { + Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(slot, rgb)) + }) + } Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some( Message::GraphicAttributes(GraphicAttributesMsg::LineColor(color)), ), diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index c6d45b677..7d153b356 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -35,6 +35,11 @@ impl OpenCADStudio { Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(), Some(K::PointCloudColorMap) => crate::t!("Point Cloud Color Map").into_owned(), Some(K::PcSection) => crate::t!("Extract Section Lines from Point Cloud").into_owned(), + Some(K::ViewBackground) => format!( + "{} — {}", + crate::tr!("view-background", "title"), + self.view_background.as_ref().map_or("", |s| s.view.as_str()) + ), Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), @@ -510,6 +515,12 @@ impl OpenCADStudio { crate::ui::window::pdf_dialogs::view_pc_section(state, flow) }) } + super::super::ModalKind::ViewBackground => { + let state = self.view_background.as_ref()?; + sized_flow(ex, 420, 560, |flow| { + crate::ui::window::pdf_dialogs::view_background(state, flow) + }) + } super::super::ModalKind::UnderlayLayers => { let state = self.underlay_layers.as_ref()?; sized_flow(ex, 460, 520, |flow| { diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs index c29d9164b..4d3099f4f 100644 --- a/src/modules/view/view_cmd.rs +++ b/src/modules/view/view_cmd.rs @@ -226,9 +226,16 @@ impl DashViewCommand { let t = text.trim(); match setting { Setting::Background => match keyword(t, &["Color", "Gradient", "Image", "None"]) { - // The other background types are chosen in their own dialogs. Some("None") => Self::apply(setting, &view, "NONE"), - Some(_) => CmdResult::Dispatch("-VIEW >SETTINGS".into()), + // The other types are set in the Background dialog. + Some(kind) => CmdResult::Dispatch(format!( + "-VIEW >BACKGROUND {view}\u{1f}{}", + match kind { + "Color" => "SOLID", + "Gradient" => "GRADIENT", + _ => "IMAGE", + } + )), None => { self.step = Step::SettingValue(setting, view); CmdResult::ReportError("Invalid option keyword.".into()) diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index d437bfac7..a26d85274 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -215,6 +215,11 @@ impl Scene { /// Restore camera to a named view from the document view table. pub fn restore_named_view(&mut self, view: &codec::tables::View) { use glam::Vec3; + // The model viewport takes a copy of the view's background, or none. + if self.current_layout == "Model" { + let background = copy_background(&mut self.document, view.background_handle); + self.set_active_background(background); + } let cam = &mut *self.camera.borrow_mut(); // The stored direction points from the target toward the eye. let eye_dir = Vec3::new( @@ -245,6 +250,22 @@ impl Scene { self.camera_generation += 1; } + /// The background the active model-space viewport shows (null for the + /// drawing area colour). + pub fn active_background(&self) -> Handle { + self.active_tile_vport_handle() + .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) + .map_or(Handle::NULL, |value| value.background_handle) + } + + fn set_active_background(&mut self, background: Handle) { + let Some(handle) = self.active_tile_vport_handle() else { return }; + if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { + value.background_handle = background; + self.camera_generation += 1; + } + } + /// Save the current camera state into a new named view entry. /// Returns the view; caller must push it into document.views. pub fn current_as_named_view(&self, name: &str) -> codec::tables::View { @@ -1638,3 +1659,68 @@ impl Scene { } } } + +/// The drawing's ACAD_BACKGROUND dictionary, made when it has none. +fn background_dictionary(document: &mut codec::CadDocument) -> Option { + use codec::objects::{Dictionary, ObjectType}; + let root = document.header.named_objects_dict_handle; + let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else { + return None; + }; + if let Some(existing) = root_dictionary.get("ACAD_BACKGROUND") { + if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) { + return Some(existing); + } + } + let handle = document.allocate_handle(); + let mut dictionary = Dictionary::new(); + dictionary.handle = handle; + dictionary.owner = root; + dictionary.reactors.push(root); + document.objects.insert(handle, ObjectType::Dictionary(dictionary)); + if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) { + root_dictionary.add_entry("ACAD_BACKGROUND", handle); + } + Some(handle) +} + +/// A new background object in ACAD_BACKGROUND, keyed by its own handle as +/// the reference keys it. Null when the drawing has no named-objects root. +pub fn add_background( + document: &mut codec::CadDocument, + data: codec::objects::ClassObjectData, +) -> Handle { + use codec::objects::{ClassObject, ObjectType}; + let Some(dictionary) = background_dictionary(document) else { + return Handle::NULL; + }; + let handle = document.allocate_handle(); + let mut object = ClassObject::new(data); + object.handle = handle; + object.owner = dictionary; + object.reactors = vec![dictionary]; + document.objects.insert(handle, ObjectType::ClassObject(object)); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) { + entries.add_entry(format!("{:X}", handle.value()), handle); + } + handle +} + +/// A copy of a background object (the reference gives each view and +/// viewport its own); null for none. +pub fn copy_background(document: &mut codec::CadDocument, handle: Handle) -> Handle { + use codec::objects::{ClassObjectData as D, ObjectType}; + match document.objects.get(&handle) { + Some(ObjectType::ClassObject(object)) + if matches!( + object.data, + D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_) + | D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_) + ) => + { + let data = object.data.clone(); + add_background(document, data) + } + _ => Handle::NULL, + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a895f6dd8..0ebed3852 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -28,6 +28,7 @@ pub enum BlockScalePolicy { // blocks and/or free functions). Pure text-move from the original mod.rs. mod boundary; mod camera_ops; +pub use camera_ops::{add_background, copy_background}; pub(crate) mod centerline; pub(crate) mod centermark; pub(crate) mod dimension_assoc; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index c91d30ae2..d1bdc2ae9 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2034,6 +2034,11 @@ text_atlas={:.1} wire_arena={:.1} block_geometry={:.1}", } impl Scene { + /// The model-space viewport record of the active tile. + pub(crate) fn active_tile_vport_handle(&self) -> Option { + self.model_tile_vport(self.active_model_tile.get()).map(|value| value.handle) + } + fn model_tile_vport(&self, index: usize) -> Option<&codec::tables::VPort> { let rect = self.model_tiles.borrow().get(index)?.rect; let lower_left = [rect.x as f64, (1.0 - rect.y - rect.height) as f64]; diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 3c1e49d26..a971b4081 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -111,6 +111,18 @@ pub enum PdfDialogMsg { ImportOk, Options, Help(&'static str), + // View background + BgKind(BgKind), + /// Open the colour window for a slot: 0 solid, 1 top, 2 middle, 3 bottom. + BgPickColor(u8), + BgColor(u8, [u8; 3]), + BgThree(bool), + BgRotation(String), + BgImage(String), + BgBrowse, + BgPosition(BgPosition), + BgAspect(bool), + BgApply, } fn msg(m: PdfDialogMsg) -> Message { @@ -2131,3 +2143,285 @@ pub fn view_pc_section<'a>( .width(sizing.width) .into() } + +// ── View background (-VIEW sEttings Background) ───────────────────────── + +/// What a named view shows behind the drawing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgKind { + None, + Solid, + Gradient, + Image, +} + +/// Where an image background sits: centred, stretched over the view, or +/// repeated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgPosition { + Center, + Stretch, + Tile, +} + +impl BgPosition { + pub const ALL: [BgPosition; 3] = [BgPosition::Center, BgPosition::Stretch, BgPosition::Tile]; +} + +impl fmt::Display for BgPosition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&match self { + BgPosition::Center => crate::tr!("view-background", "center"), + BgPosition::Stretch => crate::tr!("view-background", "stretch"), + BgPosition::Tile => crate::tr!("view-background", "tile"), + }) + } +} + +/// How an image background is scaled: to the screen, or keeping its +/// aspect ratio. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BgAspect(pub bool); + +impl fmt::Display for BgAspect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&if self.0 { + crate::tr!("view-background", "aspect") + } else { + crate::tr!("view-background", "fit") + }) + } +} + +/// The Background dialog's fields for one named view. +pub struct ViewBackgroundState { + pub view: String, + pub kind: BgKind, + pub solid: [u8; 3], + pub top: [u8; 3], + pub middle: [u8; 3], + pub bottom: [u8; 3], + pub three: bool, + pub rotation: String, + pub image: String, + pub position: BgPosition, + pub aspect: bool, + /// The drawing area colour, shown for no background. + pub canvas: [u8; 3], +} + +pub fn view_background<'a>( + state: &'a ViewBackgroundState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let kind = segmented( + vec![ + (BgKind::None, crate::tr!("view-background", "none")), + (BgKind::Solid, crate::tr!("view-background", "solid")), + (BgKind::Gradient, crate::tr!("view-background", "gradient")), + (BgKind::Image, crate::tr!("view-background", "image")), + ], + state.kind, + PdfDialogMsg::BgKind, + ); + let rgb = |c: [u8; 3]| iced::Color::from_rgb8(c[0], c[1], c[2]); + let fill = |background: Background| { + move |_: &Theme| container::Style { + background: Some(background), + border: Border { + width: 1.0, + radius: 4.0.into(), + color: iced::Color::from_rgb8(0x4a, 0x4a, 0x4a), + }, + ..Default::default() + } + }; + let note = |label: String| text(label).size(11).style(muted_style); + let preview: Element<'a, Message> = match state.kind { + BgKind::None => container(note(crate::tr!("view-background", "canvas"))) + .center(Fill) + .style(fill(Background::Color(rgb(state.canvas)))) + .into(), + BgKind::Solid => container(Space::new()) + .width(Fill) + .height(Fill) + .style(fill(Background::Color(rgb(state.solid)))) + .into(), + BgKind::Gradient => { + // Bottom colour at the start, top at the end, turned + // counter-clockwise by the rotation. + let angle = state.rotation.trim().parse::().unwrap_or(0.0); + let mut linear = iced::gradient::Linear::new(iced::Radians(-angle.to_radians())) + .add_stop(0.0, rgb(state.bottom)); + if state.three { + linear = linear.add_stop(0.5, rgb(state.middle)); + } + linear = linear.add_stop(1.0, rgb(state.top)); + container(Space::new()) + .width(Fill) + .height(Fill) + .style(fill(Background::Gradient(iced::Gradient::Linear(linear)))) + .into() + } + BgKind::Image => { + if std::path::Path::new(state.image.trim()).is_file() { + let fit = match (state.position, state.aspect) { + (BgPosition::Stretch, false) => iced::ContentFit::Fill, + (BgPosition::Stretch, true) => iced::ContentFit::Contain, + _ => iced::ContentFit::None, + }; + container( + image(image::Handle::from_path(state.image.trim())) + .content_fit(fit) + .width(Fill) + .height(Fill), + ) + .center(Fill) + .style(fill(Background::Color(rgb(state.canvas)))) + .into() + } else { + container(note(crate::tr!("view-background", "no-image"))) + .center(Fill) + .style(fill(Background::Color(iced::Color::from_rgb8(0x1a, 0x1a, 0x1a)))) + .into() + } + } + }; + let preview = card( + crate::tr!("view-background", "preview"), + container(preview).width(Fill).height(Length::Fixed(150.0)), + ); + let label = |value: String, enabled: bool| { + text(value) + .size(11) + .style(move |theme: &Theme| { + if enabled { + muted_style(theme) + } else { + text::Style { + color: Some(theme.palette().background.strong.color), + } + } + }) + .width(Length::Fixed(92.0)) + }; + let color_row = |name: String, slot: u8, color: [u8; 3], enabled: bool| -> Element<'a, Message> { + let mut pick = button(text(crate::tr!("view-background", "select")).size(11)) + .style(button_style(false)) + .padding([4, 10]); + if enabled { + pick = pick.on_press(msg(PdfDialogMsg::BgPickColor(slot))); + } + row![ + label(name, enabled), + swatch(color, 64.0, 22.0), + text(format!("{},{},{}", color[0], color[1], color[2])) + .size(11) + .style(muted_style), + Space::new().width(Fill), + pick, + ] + .spacing(8) + .align_y(iced::Center) + .into() + }; + let options: Element<'a, Message> = match state.kind { + BgKind::None => card( + crate::tr!("view-background", "info"), + note(crate::tr!("view-background", "none-info")), + ), + BgKind::Solid => card( + crate::tr!("view-background", "solid-color"), + color_row(crate::tr!("view-background", "color"), 0, state.solid, true), + ), + BgKind::Gradient => card( + crate::tr!("view-background", "gradient"), + column![ + row![ + chip( + crate::tr!("view-background", "three-colors"), + state.three, + msg(PdfDialogMsg::BgThree(!state.three)) + ), + Space::new().width(Fill), + text(crate::tr!("view-background", "rotation")) + .size(11) + .style(muted_style), + text_input("", &state.rotation) + .on_input(|v| msg(PdfDialogMsg::BgRotation(v))) + .size(11) + .padding([4, 8]) + .width(Length::Fixed(64.0)) + .style(field_style), + ] + .spacing(8) + .align_y(iced::Center), + color_row(crate::tr!("view-background", "top"), 1, state.top, true), + color_row(crate::tr!("view-background", "middle"), 2, state.middle, state.three), + color_row(crate::tr!("view-background", "bottom"), 3, state.bottom, true), + ] + .spacing(8), + ), + BgKind::Image => card( + crate::tr!("view-background", "image"), + column![ + row![ + text_input("", &state.image) + .on_input(|v| msg(PdfDialogMsg::BgImage(v))) + .size(11) + .padding([4, 8]) + .width(Fill) + .style(field_style), + button(text(crate::tr!("view-background", "browse")).size(11)) + .on_press(msg(PdfDialogMsg::BgBrowse)) + .style(button_style(false)) + .padding([4, 10]), + ] + .spacing(8) + .align_y(iced::Center), + row![ + label(crate::tr!("view-background", "position"), true), + pick_list(Some(state.position), BgPosition::ALL.to_vec(), |p: &BgPosition| { + p.to_string() + }) + .on_select(|p| msg(PdfDialogMsg::BgPosition(p))) + .text_size(12) + .padding([4, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + row![ + label(crate::tr!("view-background", "scale"), true), + pick_list( + Some(BgAspect(state.aspect)), + vec![BgAspect(false), BgAspect(true)], + |a: &BgAspect| a.to_string() + ) + .on_select(|a: BgAspect| msg(PdfDialogMsg::BgAspect(a.0))) + .text_size(12) + .padding([4, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(8), + ), + }; + column![ + kind, + preview, + options, + footer( + "background", + None, + crate::tr!("view-background", "apply").into(), + PdfDialogMsg::BgApply + ), + ] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} From 8055fa710dee402a60580dead479870565dd18ee Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:24:07 +0300 Subject: [PATCH 2/7] Restore a view's visual style; no background in 2D wireframe Restoring a named view gives the model viewport the view's visual style (the closest render mode), and a 2D wireframe viewport shows no background, as in the reference. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/layerprops.rs | 2 ++ src/app/commands/view_presets.rs | 31 +++++++++++++++++++++++++++++++ src/app/update/viewport.rs | 25 ++++++++++++++++++------- src/scene/camera_ops.rs | 7 +++++++ src/scene/view/render.rs | 6 +++++- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 03b810729..4bc57aa64 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -909,6 +909,7 @@ impl OpenCADStudio { let found = self.tabs[i].scene.document.views.get(&name).cloned(); if let Some(v) = found { self.tabs[i].scene.restore_named_view(&v); + self.apply_view_render_mode(i, &v); self.command_line.push_output( crate::tf!("View '{}' restored.", v.name).as_ref(), ); @@ -932,6 +933,7 @@ impl OpenCADStudio { let found = self.tabs[i].scene.document.views.get(&name).cloned(); if let Some(v) = found { self.tabs[i].scene.restore_named_view(&v); + self.apply_view_render_mode(i, &v); self.command_line .push_output(crate::tf!("View '{}' restored.", v.name).as_ref()); } else { diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 901deddd1..93fce787f 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -144,6 +144,7 @@ impl OpenCADStudio { }; let before = self.tabs[i].scene.active_gaze_dir(); self.tabs[i].scene.restore_named_view(&view); + self.apply_view_render_mode(i, &view); if view.ucs_associated && self.tabs[i].active_block_edit.is_none() { let world = view.ucs_x_axis == Vector3::new(1.0, 0.0, 0.0) && view.ucs_y_axis == Vector3::new(0.0, 1.0, 0.0) @@ -543,3 +544,33 @@ impl OpenCADStudio { Task::none() } } + +impl OpenCADStudio { + /// A restored view's visual style becomes the model tab's render mode + /// (the closest of OCS's modes); a view without one leaves it. + pub(crate) fn apply_view_render_mode(&mut self, i: usize, view: &codec::tables::View) { + use codec::entities::ViewportRenderMode as M; + if self.tabs[i].scene.current_layout != "Model" { + return; + } + let Some(ObjectType::VisualStyle(style)) = + self.tabs[i].scene.document.objects.get(&view.visual_style_handle) + else { + return; + }; + let (mode, label) = match style.description.as_str() { + "2dWireframe" => (M::Wireframe2D, "Wireframe 2D"), + "Wireframe" => (M::Wireframe3D, "Wireframe 3D"), + "Hidden" => (M::HiddenLine, "Hidden Line"), + "Flat" => (M::FlatShaded, "Flat Shaded"), + "FlatWithEdges" => (M::FlatShadedWithEdges, "Flat Shaded + Edges"), + "GouraudWithEdges" | "Shaded with edges" => { + (M::GouraudShadedWithEdges, "Gouraud Shaded + Edges") + } + "Gouraud" | "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" + | "X-Ray" => (M::GouraudShaded, "Gouraud Shaded"), + _ => return, + }; + self.apply_render_mode(i, mode, label); + } +} diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 2f81f6abe..2fdb1be62 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1090,6 +1090,24 @@ impl OpenCADStudio { .push_output(crate::tf!("Viewport visual style: {label}").as_ref()); return Task::none(); } + self.apply_render_mode(i, mode, label); + self.command_line + .push_output(crate::tf!("Visual style: {label}").as_ref()); + // The point cloud move gizmo follows the visual style. + if i == self.active_tab { + self.refresh_selected_grips(); + } + Task::none() + } + + /// The model tab's visual style, without a message. + pub(crate) fn apply_render_mode( + &mut self, + i: usize, + mode: codec::entities::ViewportRenderMode, + label: &str, + ) { + use codec::entities::ViewportRenderMode as M; self.tabs[i].render_mode = mode; // Write the style onto the active Model tile alone so it // sticks when that tile loses focus and the other tiles keep @@ -1105,13 +1123,6 @@ impl OpenCADStudio { // pipeline keys its upload cache off `geometry_epoch`. self.tabs[i].scene.bump_geometry_no_blocks(); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("Visual style: {label}").as_ref()); - // The point cloud move gizmo follows the visual style. - if i == self.active_tab { - self.refresh_selected_grips(); - } - Task::none() } pub(super) fn on_cursor_moved( diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index a26d85274..0338d4970 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -219,6 +219,13 @@ impl Scene { if self.current_layout == "Model" { let background = copy_background(&mut self.document, view.background_handle); self.set_active_background(background); + if view.visual_style_handle.is_valid() { + if let Some(handle) = self.active_tile_vport_handle() { + if let Some(value) = self.document.vports.iter_mut().find(|v| v.handle == handle) { + value.visual_style_handle = view.visual_style_handle; + } + } + } } let cam = &mut *self.camera.borrow_mut(); // The stored direction points from the target toward the eye. diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index d1bdc2ae9..34c45ff39 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2571,11 +2571,15 @@ impl Scene { } else if self.current_layout == "Model" { let index = viewport.tile_idx.unwrap_or(0); if let Some(value) = self.model_tile_vport(index) { + // A 2D wireframe view shows no background, as in the reference. + let flat = self.model_tiles.borrow().get(index).is_some_and(|tile| { + tile.render_mode == codec::entities::ViewportRenderMode::Wireframe2D + }); return build( value.visual_style_handle, value.brightness, value.contrast, - value.background_handle, + if flat { Handle::NULL } else { value.background_handle }, ); } } From 46daf03bc9217ec09efd647142d8d5469077dc59 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:37:53 +0300 Subject: [PATCH 3/7] Draw gradient backgrounds as the reference does The rotation is stored in degrees and turns the bottom-to-top direction counter-clockwise, measured in pixels; a two-colour gradient blends from the bottom colour to the top one across the whole viewport. Co-Authored-By: Claude Opus 5.5 --- src/scene/view/render.rs | 3 ++- src/shaders/background.wgsl | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 34c45ff39..e2e4d301b 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2982,7 +2982,8 @@ impl Scene { 2.0, background.horizon as f32, background.height as f32, - background.rotation as f32, + // Stored in degrees. + (background.rotation as f32).to_radians(), ]; result } diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 3c933c0a9..7be6f1229 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -108,15 +108,26 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if mode == 1u { color = u.viewport_background.rgb; } else if mode == 2u { + // Bottom to top across the viewport, the "up" direction turned + // counter-clockwise by the rotation; in pixels, so the angle holds + // whatever the viewport's shape, and spanning corner to corner. let angle = u.background_params.w; - let axis = vec2(-sin(angle), cos(angle)); - let coordinate = dot(in.uv - vec2(0.5), axis) + 0.5; + let up = vec2(-sin(angle), cos(angle)); + let size = max(u.viewport_size, vec2(1.0)); + let pixel = (vec2(in.uv.x, 1.0 - in.uv.y) - vec2(0.5)) * size; + let extent = max(abs(up.x) * size.x + abs(up.y) * size.y, 1.0); + let coordinate = clamp(dot(pixel, up) / extent + 0.5, 0.0, 1.0); let horizon = clamp(u.background_params.y, 0.0, 1.0); - let half_height = max(abs(u.background_params.z) * 0.5, 1e-3); - let lower = smoothstep(horizon - half_height, horizon, coordinate); - let upper = smoothstep(horizon, horizon + half_height, coordinate); - let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); - color = mix(low_color, u.background_top.rgb, upper); + let half_height = abs(u.background_params.z) * 0.5; + if half_height <= 1e-6 { + // Two colours: one blend from the bottom to the top. + color = mix(u.background_bottom.rgb, u.background_top.rgb, coordinate); + } else { + let lower = smoothstep(horizon - half_height, horizon, coordinate); + let upper = smoothstep(horizon, horizon + half_height, coordinate); + let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); + color = mix(low_color, u.background_top.rgb, upper); + } } else if mode == 3u { let direction = world_direction(in.ndc); if direction.z >= 0.0 { From bd4029fb5096047e1258727282864bc547c0dcc0 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:51:39 +0300 Subject: [PATCH 4/7] Blend gradient backgrounds linearly; two colours over twice the view Measured against the reference: a two-colour gradient reaches its end colours a viewport beyond the edges, and the bands blend linearly. Co-Authored-By: Claude Opus 5.5 --- src/shaders/background.wgsl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 7be6f1229..c99da6201 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -120,11 +120,13 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let horizon = clamp(u.background_params.y, 0.0, 1.0); let half_height = abs(u.background_params.z) * 0.5; if half_height <= 1e-6 { - // Two colours: one blend from the bottom to the top. - color = mix(u.background_bottom.rgb, u.background_top.rgb, coordinate); + // Two colours: one blend from the bottom to the top, over twice + // the viewport (the reference's two-colour gradient reaches the + // end colours a viewport beyond the edges). + color = mix(u.background_bottom.rgb, u.background_top.rgb, 0.25 + 0.5 * coordinate); } else { - let lower = smoothstep(horizon - half_height, horizon, coordinate); - let upper = smoothstep(horizon, horizon + half_height, coordinate); + let lower = clamp((coordinate - (horizon - half_height)) / half_height, 0.0, 1.0); + let upper = clamp((coordinate - horizon) / half_height, 0.0, 1.0); let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); color = mix(low_color, u.background_top.rgb, upper); } From 7a0e08129f43cb7d9fc86f1ad5ad21154bac03f7 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:04:52 +0300 Subject: [PATCH 5/7] Draw a centred image background at its pixel size An image background not fitted to the screen keeps its own pixel size (times its scale), centred on the viewport and moved by its offset, as the reference draws it; tiling repeats it from there. Co-Authored-By: Claude Opus 5.5 --- src/scene/view/render.rs | 6 ++++-- src/shaders/background.wgsl | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index e2e4d301b..c1e80b531 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -3009,11 +3009,13 @@ impl Scene { background.use_tiling as u8 as f32, image.width as f32 / image.height.max(1) as f32, ]; + // Offset, then the image's size in pixels at its scale: + // an image not fitted to the screen keeps its pixel size. result.image_transform = [ background.offset.x as f32, background.offset.y as f32, - background.scale.x as f32, - background.scale.y as f32, + background.scale.x as f32 * image.width as f32, + background.scale.y as f32 * image.height as f32, ]; result.params[0] = 4.0; result.image = Some(image); diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index c99da6201..25a004c50 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -81,8 +81,12 @@ fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { fn image_uv(source_uv: vec2) -> vec2 { var uv = source_uv; if u.background_image_params.x < 0.5 { - uv = (source_uv - u.background_image_transform.xy) - / max(abs(u.background_image_transform.zw), vec2(1e-6)); + // Not fitted: the image at its own pixel size (times its scale), + // centred on the viewport and moved by its offset. + let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)); + uv = pixel / max(abs(u.background_image_transform.zw), vec2(1e-6)) + + vec2(0.5) - u.background_image_transform.xy; + return select(uv, fract(uv), u.background_image_params.z > 0.5); } if u.background_image_params.y > 0.5 { let image_aspect = max(u.background_image_params.w, 1e-6); From 1cf13971dcf9b02f8f1996d3a737c0a8fbeddb0e Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:19:03 +0300 Subject: [PATCH 6/7] Keep a centred image background at its device pixel size Co-Authored-By: Claude Opus 5.5 --- src/scene/view/render.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index c1e80b531..297f5dffe 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -431,7 +431,12 @@ impl shader::Primitive for Primitive { vp.background_image.as_ref(), vp.environment_image.as_ref(), ); - inner.upload_uniforms(device, queue, &vp.uniforms); + // The scene works in logical pixels; an image background keeps + // its size in device pixels. + let mut uniforms = vp.uniforms; + uniforms.background_image_transform[2] /= scale.max(1e-3); + uniforms.background_image_transform[3] /= scale.max(1e-3); + inner.upload_uniforms(device, queue, &uniforms); // ── Scene-render cache ──────────────────────────────────────── // A pure cursor move — or any frame where the view, geometry, From 9ef8f4bedfb5e7652a4a10882aaa8f1627665193 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:31:01 +0300 Subject: [PATCH 7/7] View backgrounds: reference blend, image adjust, layouts, cleanup, style name - Gradients: the middle colour sits (horizon - 0.5) viewport widths from the centre and blends over height viewport heights each side; two colours blend over a viewport height each side. - Image backgrounds: offset in device pixels (right, down), negative scale mirrors, fitted with aspect ratio covers the viewport; the dialog keeps an image's offset and scale. - Layouts: a model-space view restored on a sheet asks for the viewport, enters it and gives it the view's camera, background and visual style; inside a viewport the view goes there; a paper-space view's background is kept on the sheet viewport but not drawn; floating viewports draw their background. - Background objects no view or viewport uses any more are erased; a view without a background restored where the viewport already is leaves the viewport's background. - The viewport's named visual style (Realistic, ...) shows by name and the render-mode picker records its style on the viewport record. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/layerprops.rs | 3 +- src/app/commands/view_presets.rs | 101 ++++++++++++++++++---- src/app/update/viewport.rs | 4 + src/app/view/controls.rs | 6 +- src/app/view/mod.rs | 14 +++ src/modules/view/view_cmd.rs | 33 ++++++- src/modules/view/visual_style.rs | 32 +++++++ src/scene/camera_ops.rs | 143 +++++++++++++++++++++++++++++-- src/scene/mod.rs | 2 +- src/scene/view/render.rs | 24 +++--- src/shaders/background.wgsl | 54 ++++++------ src/ui/window/pdf_dialogs.rs | 3 + 12 files changed, 349 insertions(+), 70 deletions(-) diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 4bc57aa64..b69df6920 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -891,7 +891,8 @@ impl OpenCADStudio { self.command_line .push_error(crate::t!("Usage: VIEW DELETE ").as_ref()); } else { - if self.tabs[i].scene.document.views.remove(&name).is_some() { + if let Some(view) = self.tabs[i].scene.document.views.remove(&name) { + crate::scene::erase_background(&mut self.tabs[i].scene.document, view.background_handle); self.command_line .push_output(crate::tf!("View '{}' deleted.", name).as_ref()); } else { diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 93fce787f..fb2cb46a1 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -53,12 +53,33 @@ impl OpenCADStudio { .to_string(), visual_style: name_of(&visual_styles, view.visual_style_handle), live_section: name_of(&live_sections, view.live_section_handle), + paper_space: view.paper_space, }) .collect(); + let scene = &self.tabs[i].scene; + let sheet_viewports = (scene.current_layout != "Model" && scene.active_viewport.is_none()).then(|| { + scene + .layout_content_viewports() + .iter() + .filter_map(|handle| match document.get_entity(*handle) { + Some(codec::EntityType::Viewport(value)) => Some(( + handle.value(), + [ + value.center.x - value.width / 2.0, + value.center.y - value.height / 2.0, + value.center.x + value.width / 2.0, + value.center.y + value.height / 2.0, + ], + )), + _ => None, + }) + .collect() + }); ViewNames { views, visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(), live_sections: live_sections.into_iter().map(|(name, _)| name).collect(), + sheet_viewports, } } @@ -132,16 +153,40 @@ impl OpenCADStudio { if !doomed.is_empty() { self.push_undo_snapshot(i, "-VIEW"); for name in &doomed { - self.tabs[i].scene.document.views.remove(name); + let removed = self.tabs[i].scene.document.views.remove(name); + if let Some(view) = removed { + crate::scene::erase_background(&mut self.tabs[i].scene.document, view.background_handle); + } self.tabs[i].scene.document.delete_layer_state(&snapshot_name(name)); } self.tabs[i].dirty = true; } } + "RESTOREVP" => { + let (name, handle) = arg.split_once('\u{1f}').unwrap_or((arg, "")); + let Some(view) = self.tabs[i].scene.document.views.get(name).cloned() else { + return Task::none(); + }; + let Some(handle) = u64::from_str_radix(handle, 16).ok().map(Handle::new) else { + return Task::none(); + }; + // The reference enters the selected viewport and restores the + // view in it. + let task = self.update(Message::EnterViewport(handle)); + self.restore_view_in_active_viewport(i, &view); + return task; + } "RESTORE" => { let Some(view) = self.tabs[i].scene.document.views.get(arg).cloned() else { return Task::none(); }; + if self.tabs[i].scene.current_layout != "Model" + && self.tabs[i].scene.active_viewport.is_some() + && !view.paper_space + { + self.restore_view_in_active_viewport(i, &view); + return Task::none(); + } let before = self.tabs[i].scene.active_gaze_dir(); self.tabs[i].scene.restore_named_view(&view); self.apply_view_render_mode(i, &view); @@ -250,6 +295,22 @@ impl OpenCADStudio { } } + /// A model-space view restored in the entered paper-space viewport, with + /// its background and visual style. + fn restore_view_in_active_viewport(&mut self, i: usize, view: &codec::tables::View) { + let Some(viewport) = self.tabs[i].scene.active_viewport else { return }; + self.tabs[i].scene.restore_named_view_in_viewport(view, viewport); + if let Some(ObjectType::VisualStyle(style)) = + self.tabs[i].scene.document.objects.get(&view.visual_style_handle) + { + if let Some((mode, _)) = crate::modules::view::visual_style::mode_for_style_name(&style.description) { + self.tabs[i].scene.set_active_viewport_render_mode(mode); + self.tabs[i].scene.bump_geometry_no_blocks(); + } + } + self.tabs[i].dirty = true; + } + /// Add or replace a named view, keeping the record (and so its settings) /// of a view saved again under the same name. fn save_named_view(&mut self, i: usize, mut view: codec::tables::View) { @@ -257,6 +318,7 @@ impl OpenCADStudio { let background = self.tabs[i].scene.active_background(); let document = &mut self.tabs[i].scene.document; view.background_handle = crate::scene::copy_background(document, background); + let mut replaced = Handle::NULL; match document.views.get(&view.name).cloned() { Some(old) => { view.name = old.name; @@ -268,6 +330,7 @@ impl OpenCADStudio { if view.background_handle.is_null() { view.background_handle = old.background_handle; } + replaced = old.background_handle; } None => { view.handle = document.allocate_handle(); @@ -280,6 +343,7 @@ impl OpenCADStudio { } } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } @@ -290,6 +354,7 @@ impl OpenCADStudio { }; self.push_undo_snapshot(i, "-VIEW"); let document = &mut self.tabs[i].scene.document; + let mut replaced = Handle::NULL; if view.handle.is_null() { view.handle = document.allocate_handle(); } @@ -297,7 +362,7 @@ impl OpenCADStudio { list.into_iter().find(|(entry, _)| entry.eq_ignore_ascii_case(value)).map_or(Handle::NULL, |(_, h)| h) }; match setting { - "BACKGROUND" => view.background_handle = Handle::NULL, + "BACKGROUND" => replaced = std::mem::replace(&mut view.background_handle, Handle::NULL), "VISUALSTYLE" => view.visual_style_handle = find(crate::io::visual_styles::offered(document)), "SECTION" => { let sections: Vec<(String, Handle)> = document @@ -351,6 +416,7 @@ impl OpenCADStudio { _ => {} } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } } @@ -381,6 +447,8 @@ impl OpenCADStudio { image: String::new(), position: BgPosition::Center, aspect: false, + offset: [0.0, 0.0], + scale: [1.0, 1.0], canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8), }; let document = &self.tabs[i].scene.document; @@ -407,6 +475,8 @@ impl OpenCADStudio { BgPosition::Center }; state.aspect = value.maintain_aspect_ratio; + state.offset = [value.offset.x, value.offset.y]; + state.scale = [value.scale.x, value.scale.y]; } _ => {} } @@ -518,8 +588,8 @@ impl OpenCADStudio { fit_to_screen: state.position == BgPosition::Stretch, maintain_aspect_ratio: state.aspect, use_tiling: state.position == BgPosition::Tile, - offset: codec::types::Vector2::new(0.0, 0.0), - scale: codec::types::Vector2::new(1.0, 1.0), + offset: codec::types::Vector2::new(state.offset[0], state.offset[1]), + scale: codec::types::Vector2::new(state.scale[0], state.scale[1]), })) } }; @@ -533,7 +603,8 @@ impl OpenCADStudio { crate::scene::add_background(document, data) }); if let Some(view) = document.views.get_mut(&name) { - view.background_handle = handle; + let old = std::mem::replace(&mut view.background_handle, handle); + crate::scene::erase_background(document, old); } self.tabs[i].dirty = true; } @@ -549,7 +620,6 @@ impl OpenCADStudio { /// A restored view's visual style becomes the model tab's render mode /// (the closest of OCS's modes); a view without one leaves it. pub(crate) fn apply_view_render_mode(&mut self, i: usize, view: &codec::tables::View) { - use codec::entities::ViewportRenderMode as M; if self.tabs[i].scene.current_layout != "Model" { return; } @@ -558,19 +628,12 @@ impl OpenCADStudio { else { return; }; - let (mode, label) = match style.description.as_str() { - "2dWireframe" => (M::Wireframe2D, "Wireframe 2D"), - "Wireframe" => (M::Wireframe3D, "Wireframe 3D"), - "Hidden" => (M::HiddenLine, "Hidden Line"), - "Flat" => (M::FlatShaded, "Flat Shaded"), - "FlatWithEdges" => (M::FlatShadedWithEdges, "Flat Shaded + Edges"), - "GouraudWithEdges" | "Shaded with edges" => { - (M::GouraudShadedWithEdges, "Gouraud Shaded + Edges") - } - "Gouraud" | "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" - | "X-Ray" => (M::GouraudShaded, "Gouraud Shaded"), - _ => return, + use crate::modules::view::visual_style; + let Some((mode, named)) = visual_style::mode_for_style_name(&style.description) else { + return; }; - self.apply_render_mode(i, mode, label); + // A style of its own keeps its name; the render modes keep theirs. + let label = if named { style.description.clone() } else { visual_style::label_for(mode).to_string() }; + self.apply_render_mode(i, mode, &label); } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 2fdb1be62..57e627446 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1091,6 +1091,10 @@ impl OpenCADStudio { return Task::none(); } self.apply_render_mode(i, mode, label); + // The viewport record carries the style the mode is saved as. + self.tabs[i] + .scene + .set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode)); self.command_line .push_output(crate::tf!("Visual style: {label}").as_ref()); // The point cloud move gizmo follows the visual style. diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 200a1ffc3..31343993f 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -30,6 +30,7 @@ pub(super) fn viewport_controls<'a>( tile_count: usize, render_mode_menu_open: bool, render_mode_preview: Option, + style_name: Option, ) -> Element<'a, Message> { let render_modes: Vec = crate::modules::view::visual_style::VISUAL_STYLES .iter() @@ -98,7 +99,7 @@ pub(super) fn viewport_controls<'a>( // flyout keeps the current style unchanged while hovering; a click commits. let picker_head = button( row![ - text(RenderModeChoice(render_mode).to_string()).size(11), + text(style_name.unwrap_or_else(|| RenderModeChoice(render_mode).to_string())).size(11), crate::ui::icons::themed_arrow_toggle(render_mode_menu_open, 9.0), ] .spacing(6) @@ -379,7 +380,7 @@ mod tests { #[test] fn test_viewport_controls_construction() { for style in crate::modules::view::visual_style::VISUAL_STYLES { - let _el = viewport_controls(style.mode, true, true, true, 1, false, None); + let _el = viewport_controls(style.mode, true, true, true, 1, false, None, None); let _el_open = viewport_controls( style.mode, false, @@ -388,6 +389,7 @@ mod tests { 2, true, Some(M::Wireframe3D), + None, ); } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 933d30211..33841b9a3 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1297,6 +1297,18 @@ bg={bg_ms:.1}ms n={view_count}" let rect = tab.scene.active_model_tile_bounds(vw, vh); // Unified control chip: split buttons + render-mode picker + // grid / grid-snap toggles, for the active Model tile. + // A named style (Realistic, …) the tile draws as shows by name. + let style_name = match tab.scene.document.objects.get(&tab.scene.active_visual_style()) { + Some(codec::objects::ObjectType::VisualStyle(style)) => { + match crate::modules::view::visual_style::mode_for_style_name(&style.description) { + Some((mode, true)) if mode == tab.render_mode => { + Some(crate::t!(style.description.as_str()).into_owned()) + } + _ => None, + } + } + _ => None, + }; let bar = viewport_controls( tab.render_mode, self.show_grid, @@ -1305,6 +1317,7 @@ bg={bg_ms:.1}ms n={view_count}" tab.scene.model_tiles.borrow().len(), self.render_mode_menu_open, self.render_mode_preview, + style_name, ); // Adaptive: DensitySwap measures the bar's real width every frame // (reported into `render_bar_w`, which the ViewCube reads to decide @@ -1397,6 +1410,7 @@ bg={bg_ms:.1}ms n={view_count}" 0, self.render_mode_menu_open, self.render_mode_preview, + None, ); let adaptive: Element<'_, Message> = DensitySwap::new(vec![ iced::widget::opaque(bar), diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs index 4d3099f4f..a439e61ad 100644 --- a/src/modules/view/view_cmd.rs +++ b/src/modules/view/view_cmd.rs @@ -57,6 +57,8 @@ pub struct ViewInfo { pub category: String, pub visual_style: Option, pub live_section: Option, + /// Saved in paper space. + pub paper_space: bool, } /// What -VIEW checks typed names against. @@ -65,6 +67,10 @@ pub struct ViewNames { pub views: Vec, pub visual_styles: Vec, pub live_sections: Vec, + /// On a layout's sheet (no viewport entered): the layout's viewports as + /// (handle, [min x, min y, max x, max y]), one of which takes a + /// model-space view being restored. + pub sheet_viewports: Option>, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -106,6 +112,7 @@ enum Step { List, Delete, Restore, + RestoreViewport(String), Save, WindowName, WindowFirst(String), @@ -350,6 +357,7 @@ impl CadCommand for DashViewCommand { Step::List | Step::SettingViewList(_) => "Enter view name(s) to list <*>:".into(), Step::Delete => "Enter view name(s) to delete:".into(), Step::Restore => "Enter view name to restore:".into(), + Step::RestoreViewport(_) => "Select Viewport for view:".into(), Step::Save | Step::WindowName => "Enter view name to save:".into(), Step::WindowFirst(_) => "Specify first corner:".into(), Step::WindowSecond(..) => "Specify opposite corner:".into(), @@ -421,9 +429,19 @@ impl CadCommand for DashViewCommand { } } Step::Restore => match self.view(t) { + // On a sheet a model-space view goes into a viewport the + // user selects. + Some(view) if !view.paper_space && self.names.sheet_viewports.is_some() => { + self.step = Step::RestoreViewport(view.name.clone()); + CmdResult::ReportMeasurement("Restoring Model space view.".into()) + } Some(view) => CmdResult::Dispatch(format!("-VIEW >RESTORE {}", view.name)), None => Self::end(&format!("Cannot find view \"{t}\".")), }, + Step::RestoreViewport(name) => { + self.step = Step::RestoreViewport(name); + CmdResult::ReportError("*Invalid selection*".into()) + } Step::Save => CmdResult::Dispatch(format!("-VIEW >SAVE {t}")), Step::WindowName => { self.step = Step::WindowFirst(t.to_string()); @@ -494,6 +512,19 @@ impl CadCommand for DashViewCommand { "-VIEW >WINDOW {}\u{1f}{},{}\u{1f}{},{}", name, first.x, first.y, pt.x, pt.y )), + Step::RestoreViewport(name) => { + // The topmost viewport under the pick. + let hit = self.names.sheet_viewports.iter().flatten().rev().find(|(_, [x0, y0, x1, y1])| { + pt.x >= *x0 && pt.x <= *x1 && pt.y >= *y0 && pt.y <= *y1 + }); + match hit { + Some((handle, _)) => CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}")), + None => { + self.step = Step::RestoreViewport(name); + CmdResult::NeedPoint + } + } + } step => { self.step = step; CmdResult::NeedPoint @@ -503,7 +534,7 @@ impl CadCommand for DashViewCommand { fn on_enter(&mut self) -> CmdResult { match std::mem::replace(&mut self.step, Step::Main) { - Step::Main | Step::Delete | Step::Restore | Step::Save | Step::WindowName => { + Step::Main | Step::Delete | Step::Restore | Step::RestoreViewport(_) | Step::Save | Step::WindowName => { CmdResult::Dispatch("-VIEW >END".into()) } Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()), diff --git a/src/modules/view/visual_style.rs b/src/modules/view/visual_style.rs index f973038fc..9c999de89 100644 --- a/src/modules/view/visual_style.rs +++ b/src/modules/view/visual_style.rs @@ -106,6 +106,38 @@ pub fn label_for(mode: Mode) -> &'static str { .unwrap_or("Wireframe 2D") } +/// The render mode a named visual style draws as, and whether the name is a +/// style of its own (Realistic, Conceptual, …) rather than one of the modes. +pub fn mode_for_style_name(name: &str) -> Option<(Mode, bool)> { + Some(match name { + "2dWireframe" => (Mode::Wireframe2D, false), + "Wireframe" => (Mode::Wireframe3D, false), + "Hidden" => (Mode::HiddenLine, false), + "Flat" => (Mode::FlatShaded, false), + "FlatWithEdges" => (Mode::FlatShadedWithEdges, false), + "Gouraud" => (Mode::GouraudShaded, false), + "GouraudWithEdges" => (Mode::GouraudShadedWithEdges, false), + "Shaded with edges" => (Mode::GouraudShadedWithEdges, true), + "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" | "X-Ray" => { + (Mode::GouraudShaded, true) + } + _ => return None, + }) +} + +/// The name of the visual style a render mode is saved as. +pub fn style_name_for(mode: Mode) -> &'static str { + match mode { + Mode::Wireframe2D => "2dWireframe", + Mode::Wireframe3D => "Wireframe", + Mode::HiddenLine => "Hidden", + Mode::FlatShaded => "Flat", + Mode::FlatShadedWithEdges => "FlatWithEdges", + Mode::GouraudShaded => "Gouraud", + Mode::GouraudShadedWithEdges => "GouraudWithEdges", + } +} + /// The style a style keyword names. Only the seven exist; nothing maps onto a /// nearest neighbour, because there is no longer anything else to map from. pub fn mode_for_keyword(keyword: &str) -> Option { diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 0338d4970..e9dcbcc6b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -214,11 +214,28 @@ impl Scene { /// Restore camera to a named view from the document view table. pub fn restore_named_view(&mut self, view: &codec::tables::View) { - use glam::Vec3; - // The model viewport takes a copy of the view's background, or none. + let before = { + let cam = self.camera.borrow(); + (cam.rotation, cam.target, cam.distance) + }; + self.restore_named_view_camera(view); + let unchanged = { + let cam = self.camera.borrow(); + cam.rotation.abs_diff_eq(before.0, 1e-6) + && cam.target.abs_diff_eq(before.1, 1e-6 * before.1.length().max(1.0)) + && (cam.distance - before.2).abs() <= 1e-6 * before.2.abs().max(1.0) + }; + // The model viewport takes a copy of the view's background; a view + // without one clears it, unless the view is what the viewport already + // shows (the reference leaves the viewport as it is then). + if self.current_layout != "Model" && self.active_viewport.is_none() && view.paper_space { + self.set_sheet_background(view); + } if self.current_layout == "Model" { - let background = copy_background(&mut self.document, view.background_handle); - self.set_active_background(background); + if view.background_handle.is_valid() || !unchanged { + let background = copy_background(&mut self.document, view.background_handle); + self.set_active_background(background); + } if view.visual_style_handle.is_valid() { if let Some(handle) = self.active_tile_vport_handle() { if let Some(value) = self.document.vports.iter_mut().find(|v| v.handle == handle) { @@ -227,7 +244,50 @@ impl Scene { } } } - let cam = &mut *self.camera.borrow_mut(); + } + + fn restore_named_view_camera(&mut self, view: &codec::tables::View) { + named_view_camera(view, &mut self.camera.borrow_mut()); + self.camera_generation += 1; + } + + /// Restores a model-space view into a paper-space viewport: its camera, + /// a copy of the view's background (or none) and the view's visual style. + pub fn restore_named_view_in_viewport(&mut self, view: &codec::tables::View, viewport: Handle) { + let mut camera = self.navigation_camera(); + named_view_camera(view, &mut camera); + self.apply_navigation_camera(camera); + let background = copy_background(&mut self.document, view.background_handle); + let mut old = Handle::NULL; + if let Some(EntityType::Viewport(value)) = self.document.get_entity_mut(viewport) { + old = std::mem::replace(&mut value.background_handle, background); + if view.visual_style_handle.is_valid() { + value.visual_style_handle = view.visual_style_handle; + } + } + erase_background(&mut self.document, old); + self.notify_viewport_changed(viewport); + self.camera_generation += 1; + } + + /// The paper-space viewport of the current layout takes a copy of a + /// paper-space view's background (kept with the layout; the sheet itself + /// is not drawn over). + fn set_sheet_background(&mut self, view: &codec::tables::View) { + let sheet = self.current_layout_sheet_viewport_handle(); + let background = copy_background(&mut self.document, view.background_handle); + let old = match self.document.get_entity_mut(sheet) { + Some(EntityType::Viewport(value)) => std::mem::replace(&mut value.background_handle, background), + _ => background, + }; + erase_background(&mut self.document, old); + } +} + +/// Points a camera along a named view: direction, target and height. +fn named_view_camera(view: &codec::tables::View, cam: &mut view::camera::Camera) { + use glam::Vec3; + { // The stored direction points from the target toward the eye. let eye_dir = Vec3::new( view.direction.x as f32, @@ -254,22 +314,55 @@ impl Scene { } else { cam.distance }; - self.camera_generation += 1; } +} +impl Scene { /// The background the active model-space viewport shows (null for the /// drawing area colour). pub fn active_background(&self) -> Handle { + if self.current_layout != "Model" { + // Inside a viewport, that viewport's; the sheet's is not saved + // with a paper-space view. + return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) { + Some(EntityType::Viewport(value)) => value.background_handle, + _ => Handle::NULL, + }; + } self.active_tile_vport_handle() .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) .map_or(Handle::NULL, |value| value.background_handle) } + /// The visual style of the active Model tile's viewport record. + pub fn active_visual_style(&self) -> Handle { + self.active_tile_vport_handle() + .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) + .map_or(Handle::NULL, |value| value.visual_style_handle) + } + + /// Records the visual style named `name` on the active Model tile's + /// viewport record, when the drawing has it. + pub fn set_active_visual_style_named(&mut self, name: &str) { + let style = self.document.objects.iter().find_map(|(handle, object)| match object { + codec::objects::ObjectType::VisualStyle(style) if style.description == name => Some(*handle), + _ => None, + }); + let (Some(style), Some(handle)) = (style, self.active_tile_vport_handle()) else { return }; + if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { + value.visual_style_handle = style; + } + } + fn set_active_background(&mut self, background: Handle) { let Some(handle) = self.active_tile_vport_handle() else { return }; - if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { - value.background_handle = background; - self.camera_generation += 1; + let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) else { + return; + }; + let old = std::mem::replace(&mut value.background_handle, background); + self.camera_generation += 1; + if old != background { + erase_background(&mut self.document, old); } } @@ -1713,6 +1806,38 @@ pub fn add_background( handle } +/// Erases a background object once no view or viewport refers to it (the +/// reference erases a view's background when it is replaced, cleared or the +/// view deleted, and a viewport's when the viewport takes another). +pub fn erase_background(document: &mut codec::CadDocument, handle: Handle) { + use codec::objects::{ClassObjectData as D, ObjectType}; + if !handle.is_valid() + || document.views.iter().any(|view| view.background_handle == handle) + || document.vports.iter().any(|vport| vport.background_handle == handle) + || document.entities().any(|entity| { + matches!(entity, codec::EntityType::Viewport(viewport) if viewport.background_handle == handle) + }) + { + return; + } + let owner = match document.objects.get(&handle) { + Some(ObjectType::ClassObject(object)) + if matches!( + object.data, + D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_) + | D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_) + ) => + { + object.owner + } + _ => return, + }; + document.objects.remove(&handle); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&owner) { + entries.entries.retain(|(_, entry)| *entry != handle); + } +} + /// A copy of a background object (the reference gives each view and /// viewport its own); null for none. pub fn copy_background(document: &mut codec::CadDocument, handle: Handle) -> Handle { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0ebed3852..34f9804f3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -28,7 +28,7 @@ pub enum BlockScalePolicy { // blocks and/or free functions). Pure text-move from the original mod.rs. mod boundary; mod camera_ops; -pub use camera_ops::{add_background, copy_background}; +pub use camera_ops::{add_background, copy_background, erase_background}; pub(crate) mod centerline; pub(crate) mod centermark; pub(crate) mod dimension_assoc; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 297f5dffe..442f6415a 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -434,8 +434,10 @@ impl shader::Primitive for Primitive { // The scene works in logical pixels; an image background keeps // its size in device pixels. let mut uniforms = vp.uniforms; - uniforms.background_image_transform[2] /= scale.max(1e-3); - uniforms.background_image_transform[3] /= scale.max(1e-3); + // The image's size and offset are in device pixels. + for value in &mut uniforms.background_image_transform { + *value /= scale.max(1e-3); + } inner.upload_uniforms(device, queue, &uniforms); // ── Scene-render cache ──────────────────────────────────────── @@ -2557,20 +2559,18 @@ impl Scene { if viewport.paper_sheet { let handle = self.current_layout_sheet_viewport_handle(); if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) { - return build( - value.visual_style_handle, - value.brightness, - value.contrast, - value.background_handle, - ); + // The sheet keeps its page and desk: a paper-space view's + // background is kept with the layout but not drawn. + return build(value.visual_style_handle, value.brightness, value.contrast, Handle::NULL); } } else if viewport.handle.is_valid() { if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) { + let flat = value.render_mode == codec::entities::ViewportRenderMode::Wireframe2D; return build( value.visual_style_handle, value.brightness, value.contrast, - value.background_handle, + if flat { Handle::NULL } else { value.background_handle }, ); } } else if self.current_layout == "Model" { @@ -4907,7 +4907,11 @@ impl Scene { // (hatched) frame once it settles. Only applied to the on-screen // Model / paper content — the paper *sheet* keeps its fills. skip_hatch: self.hatch_lod_enabled() && navigating, - skip_background: !inst.paper_sheet && self.current_layout != "Model", + // A floating viewport shows the page behind it unless it has a + // background of its own. + skip_background: !inst.paper_sheet + && self.current_layout != "Model" + && display.background.params[0] < 0.5, geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, wire_content_id, diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 25a004c50..dd4f7671b 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -81,22 +81,25 @@ fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { fn image_uv(source_uv: vec2) -> vec2 { var uv = source_uv; if u.background_image_params.x < 0.5 { - // Not fitted: the image at its own pixel size (times its scale), - // centred on the viewport and moved by its offset. - let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)); - uv = pixel / max(abs(u.background_image_transform.zw), vec2(1e-6)) - + vec2(0.5) - u.background_image_transform.xy; + // Not fitted: the image at its own pixel size times its scale (a + // negative scale mirrors it), centred on the viewport and moved by + // its offset in pixels, right and down. + let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)) + - u.background_image_transform.xy; + let size = u.background_image_transform.zw; + let safe = select(size, vec2(1e-6), abs(size) < vec2(1e-6)); + uv = pixel / safe + vec2(0.5); return select(uv, fract(uv), u.background_image_params.z > 0.5); } + // Fitted: the image fills the viewport; keeping its aspect ratio it + // covers the viewport and the overflow is cut off evenly. if u.background_image_params.y > 0.5 { let image_aspect = max(u.background_image_params.w, 1e-6); let viewport_aspect = max(u.viewport_size.x / max(u.viewport_size.y, 1.0), 1e-6); if image_aspect > viewport_aspect { - let scale = viewport_aspect / image_aspect; - uv.y = (uv.y - 0.5) / scale + 0.5; + uv.x = (uv.x - 0.5) * (viewport_aspect / image_aspect) + 0.5; } else { - let scale = image_aspect / viewport_aspect; - uv.x = (uv.x - 0.5) / scale + 0.5; + uv.y = (uv.y - 0.5) * (image_aspect / viewport_aspect) + 0.5; } } if u.background_image_params.z > 0.5 { @@ -112,28 +115,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if mode == 1u { color = u.viewport_background.rgb; } else if mode == 2u { - // Bottom to top across the viewport, the "up" direction turned - // counter-clockwise by the rotation; in pixels, so the angle holds - // whatever the viewport's shape, and spanning corner to corner. + // Bottom to top along the "up" direction turned counter-clockwise by + // the rotation, in pixels from the viewport centre. As the reference + // draws it: the middle colour sits (horizon - 0.5) viewport widths + // from the centre and blends linearly into the top and bottom colours + // over height viewport heights on each side; two colours are one blend + // over a viewport height on each side of the centre. let angle = u.background_params.w; let up = vec2(-sin(angle), cos(angle)); let size = max(u.viewport_size, vec2(1.0)); let pixel = (vec2(in.uv.x, 1.0 - in.uv.y) - vec2(0.5)) * size; - let extent = max(abs(up.x) * size.x + abs(up.y) * size.y, 1.0); - let coordinate = clamp(dot(pixel, up) / extent + 0.5, 0.0, 1.0); - let horizon = clamp(u.background_params.y, 0.0, 1.0); - let half_height = abs(u.background_params.z) * 0.5; - if half_height <= 1e-6 { - // Two colours: one blend from the bottom to the top, over twice - // the viewport (the reference's two-colour gradient reaches the - // end colours a viewport beyond the edges). - color = mix(u.background_bottom.rgb, u.background_top.rgb, 0.25 + 0.5 * coordinate); - } else { - let lower = clamp((coordinate - (horizon - half_height)) / half_height, 0.0, 1.0); - let upper = clamp((coordinate - horizon) / half_height, 0.0, 1.0); - let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); - color = mix(low_color, u.background_top.rgb, upper); - } + let along = dot(pixel, up); + let three = abs(u.background_params.z) > 1e-6; + let middle_at = select(0.0, (u.background_params.y - 0.5) * size.x, three); + let half_band = select(1.0, abs(u.background_params.z), three) * size.y; + let middle = select(mix(u.background_bottom.rgb, u.background_top.rgb, 0.5), + u.background_middle.rgb, three); + let lower = clamp((along - middle_at + half_band) / half_band, 0.0, 1.0); + let upper = clamp((along - middle_at) / half_band, 0.0, 1.0); + color = mix(mix(u.background_bottom.rgb, middle, lower), u.background_top.rgb, upper); } else if mode == 3u { let direction = world_direction(in.ndc); if direction.z >= 0.0 { diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index a971b4081..cdf7cf97a 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -2206,6 +2206,9 @@ pub struct ViewBackgroundState { pub image: String, pub position: BgPosition, pub aspect: bool, + /// The image's offset (pixels) and scale, kept from the view's image. + pub offset: [f64; 2], + pub scale: [f64; 2], /// The drawing area colour, shown for no background. pub canvas: [u8; 3], }