View backgrounds: image colours as stored, viewport style name, adjust automation

- The background image texture is not an sRGB view: the image is drawn with
  its own colours, as the solid and gradient colours are.
- An entered paper-space viewport shows its named visual style (Realistic,
  ...) in the render-mode picker, and a mode picked there is recorded as the
  viewport's visual style.
- The view_background automation action takes offset=x,y and scale=x,y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-13 09:44:49 +03:00
1 parent b5b505ee9d
commit 064da8829e
8 files changed
+80 -19

No files matched your search

+2
View File
@@ -582,6 +582,8 @@ impl OpenCADStudio {
| PdfDialogMsg::BgBrowse
| PdfDialogMsg::BgPosition(_)
| PdfDialogMsg::BgAspect(_)
| PdfDialogMsg::BgOffset(_)
| PdfDialogMsg::BgScale(_)
| PdfDialogMsg::BgApply) => return self.update_view_background(message),
message @ (PdfDialogMsg::SecPerimeter(_)
| PdfDialogMsg::SecMaxPoints(_)
+2
View File
@@ -507,6 +507,8 @@ impl OpenCADStudio {
M::BgImage(value) => state.image = value,
M::BgPosition(value) => state.position = value,
M::BgAspect(on) => state.aspect = on,
M::BgOffset(value) => state.offset = value,
M::BgScale(value) => state.scale = value,
M::BgColor(slot, rgb) => match slot {
0 => state.solid = rgb,
1 => state.top = rgb,
+16 -1
View File
@@ -1798,7 +1798,7 @@ 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=<path>", "position=center|stretch|tile", "aspect=1",
// "image=<path>", "position=center|stretch|tile", "aspect=1", "offset=x,y" (pixels), "scale=x,y" (non-zero),
// "apply" or "cancel".
"view_background" => {
use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, PdfDialogMsg as P};
@@ -1840,6 +1840,21 @@ impl OpenCADStudio {
_ => return Err(bad()),
})),
"aspect" => Message::PdfDialog(P::BgAspect(value == "1")),
"offset" | "scale" => {
let pair: Vec<f64> = value
.split(',')
.map(|part| part.trim().parse::<f64>())
.collect::<Result<_, _>>()
.map_err(|_| bad())?;
let pair = <[f64; 2]>::try_from(pair).map_err(|_| bad())?;
if key == "offset" {
Message::PdfDialog(P::BgOffset(pair))
} else if pair.iter().all(|v| v.is_finite() && *v != 0.0) {
Message::PdfDialog(P::BgScale(pair))
} else {
return Err(bad());
}
}
_ => return Err(bad()),
}
}
+3
View File
@@ -1084,6 +1084,9 @@ impl OpenCADStudio {
// viewport, the picker drives that viewport entity's own
// render mode; the model-layout tab style is untouched.
if self.tabs[i].scene.set_active_viewport_render_mode(mode) {
self.tabs[i]
.scene
.set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode));
self.tabs[i].scene.bump_geometry_no_blocks();
self.tabs[i].dirty = true;
self.command_line
+20 -13
View File
@@ -182,6 +182,24 @@ impl std::fmt::Display for RenderModeChoice {
}
}
/// A named style (Realistic, …) the active viewport draws as, by name.
fn named_visual_style(
scene: &crate::scene::Scene,
mode: codec::entities::ViewportRenderMode,
) -> Option<String> {
match scene.document.objects.get(&scene.active_visual_style()) {
Some(codec::objects::ObjectType::VisualStyle(style)) => {
match crate::modules::view::visual_style::mode_for_style_name(&style.description) {
Some((named_mode, true)) if named_mode == mode => {
Some(crate::t!(style.description.as_str()).into_owned())
}
_ => None,
}
}
_ => None,
}
}
impl OpenCADStudio {
#[cfg(not(target_arch = "wasm32"))]
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
@@ -1297,18 +1315,7 @@ 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 style_name = named_visual_style(&tab.scene, tab.render_mode);
let bar = viewport_controls(
tab.render_mode,
self.show_grid,
@@ -1410,7 +1417,7 @@ bg={bg_ms:.1}ms n={view_count}"
0,
self.render_mode_menu_open,
self.render_mode_preview,
None,
named_visual_style(&tab.scene, vp_mode),
);
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
iced::widget::opaque(bar),
+16 -1
View File
@@ -336,6 +336,12 @@ impl Scene {
/// The visual style of the active Model tile's viewport record.
pub fn active_visual_style(&self) -> Handle {
if self.current_layout != "Model" {
return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) {
Some(EntityType::Viewport(value)) => value.visual_style_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.visual_style_handle)
@@ -348,7 +354,16 @@ impl Scene {
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 };
let Some(style) = style else { return };
if self.current_layout != "Model" {
if let Some(handle) = self.active_viewport {
if let Some(EntityType::Viewport(value)) = self.document.get_entity_mut(handle) {
value.visual_style_handle = style;
}
}
return;
}
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.visual_style_handle = style;
}
+18 -4
View File
@@ -3996,7 +3996,8 @@ analytic={:.1} regular={:.1} blocks={:.1}",
}
let upload = |label: &'static str,
image: Option<&crate::scene::model::image_model::DecodedImage>,
fallback: [u8; 4]| {
fallback: [u8; 4],
format: wgpu::TextureFormat| {
let (width, height, pixels) = image
.map(|image| (image.width, image.height, image.pixels.as_slice()))
.unwrap_or((1, 1, fallback.as_slice()));
@@ -4010,7 +4011,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
@@ -4030,8 +4031,21 @@ analytic={:.1} regular={:.1} blocks={:.1}",
);
texture
};
self.background_texture = upload("background.texture", background, [0, 0, 0, 255]);
self.environment_texture = upload("environment.texture", environment, [128, 128, 128, 255]);
// The background image is drawn as it is, like the solid and gradient
// colours (an sRGB view would darken it a second time); the
// environment map stays sRGB for lighting.
self.background_texture = upload(
"background.texture",
background,
[0, 0, 0, 255],
wgpu::TextureFormat::Rgba8Unorm,
);
self.environment_texture = upload(
"environment.texture",
environment,
[128, 128, 128, 255],
wgpu::TextureFormat::Rgba8UnormSrgb,
);
self.rebuild_frame_bind_group(device);
self.background_source_id = background_id;
self.environment_source_id = environment_id;
+3
View File
@@ -122,6 +122,9 @@ pub enum PdfDialogMsg {
BgBrowse,
BgPosition(BgPosition),
BgAspect(bool),
/// An image background's offset (pixels) and scale.
BgOffset([f64; 2]),
BgScale([f64; 2]),
BgApply,
}