diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 67b489df6..a84f8ffc8 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6323,3 +6323,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 b96d9718d..543c66c93 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 835564980..e573ce937 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6308,3 +6308,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 7ea3de37c..b31c44a38 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6309,3 +6309,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 e0af16f27..358f5399e 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 1bc633500..7c534a2b0 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6310,3 +6310,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 3f74818ac..6c0831926 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 cd7a95a54..1aa58081d 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6308,3 +6308,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 2c03da497..a6c5a5ca4 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 3d60dbb71..051ecf45d 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6299,3 +6299,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 f2c21d2b1..98ce35472 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6295,3 +6295,35 @@ property-patch = .patch-05 = 3Dconnexion-beállítások megnyitása… .patch-06 = Tizedesjel szerint .patch-07 = Műveleti jelek szerint + +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 f9f38edf2..eb4da87ed 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6310,3 +6310,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 029b4d86c..57a2db5df 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6299,3 +6299,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 615511916..8f3cb9d03 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6310,3 +6310,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 f8a6a9599..06f0e9df0 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 98babf286..4e5428981 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6309,3 +6309,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 590942e3e..7902fd682 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6310,3 +6310,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 3a9023b7d..bda3098fb 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6311,3 +6311,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 413738788..5c060d07e 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6312,3 +6312,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 44ca2112b..f8fff2e47 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6232,3 +6232,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 967e90b92..93eba93e8 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6294,3 +6294,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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 c75ab7c4e..f52b4f1f1 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6306,3 +6306,35 @@ property-patch = .patch-05 = Open 3Dconnexion settings... .patch-06 = Decimal Separator .patch-07 = Operational Symbols + +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/layerprops.rs b/src/app/commands/layerprops.rs index 03b810729..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 { @@ -909,6 +910,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 +934,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/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..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, } } @@ -111,7 +132,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 { @@ -132,18 +153,43 @@ 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); 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) @@ -197,6 +243,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(); @@ -241,18 +295,42 @@ 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) { 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); + let mut replaced = Handle::NULL; 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; + } + replaced = old.background_handle; } None => { view.handle = document.allocate_handle(); @@ -265,6 +343,7 @@ impl OpenCADStudio { } } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } @@ -275,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(); } @@ -282,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 @@ -336,6 +416,224 @@ impl OpenCADStudio { _ => {} } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); 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, + 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; + 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; + state.offset = [value.offset.x, value.offset.y]; + state.scale = [value.scale.x, value.scale.y]; + } + _ => {} + } + } + 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(state.offset[0], state.offset[1]), + scale: codec::types::Vector2::new(state.scale[0], state.scale[1]), + })) + } + }; + 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) { + let old = std::mem::replace(&mut view.background_handle, handle); + crate::scene::erase_background(document, old); + } + self.tabs[i].dirty = true; + } + return self.run_dash_view("SETTINGS", i); + } + _ => {} + } + 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) { + 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; + }; + use crate::modules::view::visual_style; + let Some((mode, named)) = visual_style::mode_for_style_name(&style.description) else { + return; + }; + // 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/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 000ddec0c..9127d864c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -567,6 +567,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, @@ -1775,6 +1777,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 @@ -2115,6 +2119,7 @@ pub enum ModalKind { PointCloudAttach, PointCloudColorMap, PcSection, + ViewBackground, UnderlayLayers, PdfImportSettings, PdfImportFile, @@ -4494,6 +4499,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 11c0608e5..2526cd8de 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -9315,6 +9315,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/update/viewport.rs b/src/app/update/viewport.rs index 2f81f6abe..57e627446 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1090,6 +1090,28 @@ impl OpenCADStudio { .push_output(crate::tf!("Viewport visual style: {label}").as_ref()); 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. + 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 +1127,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/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 7714bf6e1..0d8b7a9e8 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/app/view/modal.rs b/src/app/view/modal.rs index 8e297f43f..c27fbe3e1 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(), @@ -512,6 +517,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..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), @@ -226,9 +233,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()) @@ -343,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(), @@ -414,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()); @@ -487,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 @@ -496,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 d437bfac7..e9dcbcc6b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -214,8 +214,80 @@ 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; - let cam = &mut *self.camera.borrow_mut(); + 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" { + 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) { + value.visual_style_handle = view.visual_style_handle; + } + } + } + } + } + + 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, @@ -242,7 +314,56 @@ impl Scene { } else { cam.distance }; + } +} + +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 }; + 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); + } } /// Save the current camera state into a new named view entry. @@ -1638,3 +1759,100 @@ 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 +} + +/// 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 { + 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 7799d9ef3..3a96d43e7 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -31,6 +31,7 @@ pub enum BlockScalePolicy { mod block_base_point_tests; mod boundary; mod camera_ops; +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 c91d30ae2..442f6415a 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -431,7 +431,14 @@ 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; + // 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 ──────────────────────────────────────── // A pure cursor move — or any frame where the view, geometry, @@ -2034,6 +2041,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]; @@ -2547,30 +2559,32 @@ 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" { 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 }, ); } } @@ -2973,7 +2987,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 } @@ -2999,11 +3014,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); @@ -4890,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 3c933c0a9..dd4f7671b 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -81,18 +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 { - 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 (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 { @@ -108,15 +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 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 axis = vec2(-sin(angle), cos(angle)); - let coordinate = dot(in.uv - vec2(0.5), axis) + 0.5; - 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 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 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 3c1e49d26..cdf7cf97a 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,288 @@ 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 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], +} + +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() +}