forked from 1b-agent/opencadstudio-upstream
Background dialog preview: mirrored images, device pixels; faint inactive values
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0ee95fa4ba
commit
2a808c4526
4 files changed
+61
-9
No files matched your search
@@ -453,7 +453,7 @@ impl OpenCADStudio {
|
||||
offset: ["0".into(), "0".into()],
|
||||
scale: ["1".into(), "1".into()],
|
||||
scale_base: [1.0, 1.0],
|
||||
image_size: None,
|
||||
preview: None,
|
||||
viewport: {
|
||||
let (width, height) = self.tabs[i].scene.selection.borrow().view.vp_size;
|
||||
[width as f32, height as f32]
|
||||
@@ -487,7 +487,7 @@ impl OpenCADStudio {
|
||||
state.offset = [value.offset.x, value.offset.y].map(crate::ui::window::pdf_dialogs::short);
|
||||
state.scale = [value.scale.x, value.scale.y].map(crate::ui::window::pdf_dialogs::short);
|
||||
state.scale_base = [value.scale.x, value.scale.y];
|
||||
state.image_size = image::image_dimensions(&value.filename).ok().map(|(w, h)| [w, h]);
|
||||
state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value.filename);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -513,7 +513,7 @@ impl OpenCADStudio {
|
||||
M::BgThree(on) => state.three = on,
|
||||
M::BgRotation(value) => state.rotation = value,
|
||||
M::BgImage(value) => {
|
||||
state.image_size = image::image_dimensions(value.trim()).ok().map(|(w, h)| [w, h]);
|
||||
state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value);
|
||||
state.image = value;
|
||||
}
|
||||
M::BgPosition(value) => state.position = value,
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod dispatch;
|
||||
pub mod render;
|
||||
pub mod transform;
|
||||
pub mod viewport_pane;
|
||||
pub use render::device_scale;
|
||||
@@ -385,6 +385,7 @@ impl shader::Primitive for Primitive {
|
||||
pipeline.gpu_error_epoch = errors_at_entry;
|
||||
}
|
||||
let scale = viewport.scale_factor() as f32;
|
||||
DEVICE_SCALE.store(scale.to_bits(), std::sync::atomic::Ordering::Relaxed);
|
||||
let instance_ids: Vec<u64> = self.viewports.iter().map(|vp| vp.instance_id).collect();
|
||||
let slots = pipeline.resolve_slots(device, queue, &instance_ids);
|
||||
for (i, vp) in self.viewports.iter().enumerate() {
|
||||
@@ -5440,3 +5441,11 @@ mod layer0_inherit_tests {
|
||||
assert!(scene.document_render_env_cache.borrow().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
/// The display's device pixels per logical pixel, as the viewport last drew.
|
||||
static DEVICE_SCALE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0x3f80_0000);
|
||||
|
||||
/// Device pixels per logical pixel of the drawing viewport.
|
||||
pub fn device_scale() -> f32 {
|
||||
f32::from_bits(DEVICE_SCALE.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
@@ -2185,6 +2185,25 @@ impl fmt::Display for BgPosition {
|
||||
}
|
||||
}
|
||||
|
||||
/// A background image as the preview draws it.
|
||||
pub struct BgPreview {
|
||||
pub size: [u32; 2],
|
||||
/// Unmirrored, mirrored in x, in y, in both.
|
||||
pub handles: [image::Handle; 4],
|
||||
}
|
||||
|
||||
/// Loads a background image for the preview, at most 512 pixels a side.
|
||||
pub fn bg_preview(path: &str) -> Option<BgPreview> {
|
||||
let decoded = ::image::open(path.trim()).ok()?;
|
||||
let size = [decoded.width(), decoded.height()];
|
||||
let small = if size[0].max(size[1]) > 512 { decoded.thumbnail(512, 512) } else { decoded }.to_rgba8();
|
||||
let handle = |pixels: ::image::RgbaImage| image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw());
|
||||
let flip_x = ::image::imageops::flip_horizontal(&small);
|
||||
let flip_y = ::image::imageops::flip_vertical(&small);
|
||||
let flip_xy = ::image::imageops::flip_vertical(&flip_x);
|
||||
Some(BgPreview { size, handles: [handle(small), handle(flip_x), handle(flip_y), handle(flip_xy)] })
|
||||
}
|
||||
|
||||
/// Image offsets the reference's Adjust Image sliders reach, in pixels.
|
||||
pub const BG_OFFSET_LIMIT: f64 = 2000.0;
|
||||
/// Image scales the reference's sliders reach (slider value / 10).
|
||||
@@ -2222,8 +2241,9 @@ pub struct ViewBackgroundState {
|
||||
pub offset: [String; 2],
|
||||
pub scale: [String; 2],
|
||||
pub scale_base: [f64; 2],
|
||||
/// The chosen image's size in pixels, for the preview.
|
||||
pub image_size: Option<[u32; 2]>,
|
||||
/// The chosen image for the preview: its size in pixels and a small copy
|
||||
/// as it is and mirrored (x, y, both), for negative scales.
|
||||
pub preview: Option<BgPreview>,
|
||||
/// The drawing viewport's size, which the preview stands for.
|
||||
pub viewport: [f32; 2],
|
||||
/// The drawing area colour, shown for no background.
|
||||
@@ -2285,12 +2305,15 @@ pub fn view_background<'a>(
|
||||
}
|
||||
BgKind::Image => {
|
||||
if std::path::Path::new(state.image.trim()).is_file() {
|
||||
let handle = image::Handle::from_path(state.image.trim());
|
||||
let canvas = Background::Color(rgb(state.canvas));
|
||||
if state.position == BgPosition::Stretch {
|
||||
// Filling the view; keeping the aspect ratio it covers
|
||||
// the view and the overflow is cut off.
|
||||
let fit = if state.aspect { iced::ContentFit::Cover } else { iced::ContentFit::Fill };
|
||||
let handle = state.preview.as_ref().map_or_else(
|
||||
|| image::Handle::from_path(state.image.trim()),
|
||||
|preview| preview.handles[0].clone(),
|
||||
);
|
||||
container(image(handle).content_fit(fit).width(Fill).height(Fill))
|
||||
.center(Fill)
|
||||
.clip(true)
|
||||
@@ -2305,11 +2328,20 @@ pub fn view_background<'a>(
|
||||
let v = parse(&state.scale[k], state.scale_base[k]);
|
||||
if v.abs() < 1e-9 { state.scale_base[k] } else { v }
|
||||
});
|
||||
let [iw, ih] = state.image_size.unwrap_or([1, 1]);
|
||||
let [iw, ih] = state.preview.as_ref().map_or([1, 1], |preview| preview.size);
|
||||
// A negative scale mirrors the image.
|
||||
let mirrored = usize::from(scale[0] < 0.0) | usize::from(scale[1] < 0.0) << 1;
|
||||
let handle = state.preview.as_ref().map_or_else(
|
||||
|| image::Handle::from_path(state.image.trim()),
|
||||
|preview| preview.handles[mirrored].clone(),
|
||||
);
|
||||
let viewport = state.viewport;
|
||||
// The image's pixels and offsets are device pixels; the
|
||||
// viewport is measured in logical ones.
|
||||
let device = crate::scene::view::device_scale().max(0.1);
|
||||
let tile = state.position == BgPosition::Tile;
|
||||
container(iced::widget::responsive(move |size| {
|
||||
let factor = size.height / viewport[1].max(1.0);
|
||||
let factor = size.height / viewport[1].max(1.0) / device;
|
||||
let w = (iw as f32 * scale[0].abs() as f32 * factor).max(1.0);
|
||||
let h = (ih as f32 * scale[1].abs() as f32 * factor).max(1.0);
|
||||
let x = size.width / 2.0 + offset[0] as f32 * factor - w / 2.0;
|
||||
@@ -2502,7 +2534,17 @@ fn adjust_card<'a>(state: &'a ViewBackgroundState) -> Element<'a, Message> {
|
||||
};
|
||||
let spin = |field: u8| -> Element<'a, Message> {
|
||||
let value = if field < 2 { &state.offset[field as usize] } else { &state.scale[field as usize - 2] };
|
||||
let mut input = text_input("", value).size(11).padding([4, 8]).width(Length::Fixed(64.0)).style(field_style);
|
||||
let mut input = text_input("", value)
|
||||
.size(11)
|
||||
.padding([4, 8])
|
||||
.width(Length::Fixed(64.0))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut style = field_style(theme, status);
|
||||
if !enabled {
|
||||
style.value = theme.palette().background.strong.color;
|
||||
}
|
||||
style
|
||||
});
|
||||
if enabled {
|
||||
input = input.on_input(move |v| {
|
||||
if field < 2 {
|
||||
|
||||
Reference in new issue
Block a user