diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 4bc57aa64..b69df6920 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -891,7 +891,8 @@ impl OpenCADStudio { self.command_line .push_error(crate::t!("Usage: VIEW DELETE ").as_ref()); } else { - if self.tabs[i].scene.document.views.remove(&name).is_some() { + if let Some(view) = self.tabs[i].scene.document.views.remove(&name) { + crate::scene::erase_background(&mut self.tabs[i].scene.document, view.background_handle); self.command_line .push_output(crate::tf!("View '{}' deleted.", name).as_ref()); } else { diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 93fce787f..fb2cb46a1 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -53,12 +53,33 @@ impl OpenCADStudio { .to_string(), visual_style: name_of(&visual_styles, view.visual_style_handle), live_section: name_of(&live_sections, view.live_section_handle), + paper_space: view.paper_space, }) .collect(); + let scene = &self.tabs[i].scene; + let sheet_viewports = (scene.current_layout != "Model" && scene.active_viewport.is_none()).then(|| { + scene + .layout_content_viewports() + .iter() + .filter_map(|handle| match document.get_entity(*handle) { + Some(codec::EntityType::Viewport(value)) => Some(( + handle.value(), + [ + value.center.x - value.width / 2.0, + value.center.y - value.height / 2.0, + value.center.x + value.width / 2.0, + value.center.y + value.height / 2.0, + ], + )), + _ => None, + }) + .collect() + }); ViewNames { views, visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(), live_sections: live_sections.into_iter().map(|(name, _)| name).collect(), + sheet_viewports, } } @@ -132,16 +153,40 @@ impl OpenCADStudio { if !doomed.is_empty() { self.push_undo_snapshot(i, "-VIEW"); for name in &doomed { - self.tabs[i].scene.document.views.remove(name); + let removed = self.tabs[i].scene.document.views.remove(name); + if let Some(view) = removed { + crate::scene::erase_background(&mut self.tabs[i].scene.document, view.background_handle); + } self.tabs[i].scene.document.delete_layer_state(&snapshot_name(name)); } self.tabs[i].dirty = true; } } + "RESTOREVP" => { + let (name, handle) = arg.split_once('\u{1f}').unwrap_or((arg, "")); + let Some(view) = self.tabs[i].scene.document.views.get(name).cloned() else { + return Task::none(); + }; + let Some(handle) = u64::from_str_radix(handle, 16).ok().map(Handle::new) else { + return Task::none(); + }; + // The reference enters the selected viewport and restores the + // view in it. + let task = self.update(Message::EnterViewport(handle)); + self.restore_view_in_active_viewport(i, &view); + return task; + } "RESTORE" => { let Some(view) = self.tabs[i].scene.document.views.get(arg).cloned() else { return Task::none(); }; + if self.tabs[i].scene.current_layout != "Model" + && self.tabs[i].scene.active_viewport.is_some() + && !view.paper_space + { + self.restore_view_in_active_viewport(i, &view); + return Task::none(); + } let before = self.tabs[i].scene.active_gaze_dir(); self.tabs[i].scene.restore_named_view(&view); self.apply_view_render_mode(i, &view); @@ -250,6 +295,22 @@ impl OpenCADStudio { } } + /// A model-space view restored in the entered paper-space viewport, with + /// its background and visual style. + fn restore_view_in_active_viewport(&mut self, i: usize, view: &codec::tables::View) { + let Some(viewport) = self.tabs[i].scene.active_viewport else { return }; + self.tabs[i].scene.restore_named_view_in_viewport(view, viewport); + if let Some(ObjectType::VisualStyle(style)) = + self.tabs[i].scene.document.objects.get(&view.visual_style_handle) + { + if let Some((mode, _)) = crate::modules::view::visual_style::mode_for_style_name(&style.description) { + self.tabs[i].scene.set_active_viewport_render_mode(mode); + self.tabs[i].scene.bump_geometry_no_blocks(); + } + } + self.tabs[i].dirty = true; + } + /// Add or replace a named view, keeping the record (and so its settings) /// of a view saved again under the same name. fn save_named_view(&mut self, i: usize, mut view: codec::tables::View) { @@ -257,6 +318,7 @@ impl OpenCADStudio { let background = self.tabs[i].scene.active_background(); let document = &mut self.tabs[i].scene.document; view.background_handle = crate::scene::copy_background(document, background); + let mut replaced = Handle::NULL; match document.views.get(&view.name).cloned() { Some(old) => { view.name = old.name; @@ -268,6 +330,7 @@ impl OpenCADStudio { if view.background_handle.is_null() { view.background_handle = old.background_handle; } + replaced = old.background_handle; } None => { view.handle = document.allocate_handle(); @@ -280,6 +343,7 @@ impl OpenCADStudio { } } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } @@ -290,6 +354,7 @@ impl OpenCADStudio { }; self.push_undo_snapshot(i, "-VIEW"); let document = &mut self.tabs[i].scene.document; + let mut replaced = Handle::NULL; if view.handle.is_null() { view.handle = document.allocate_handle(); } @@ -297,7 +362,7 @@ impl OpenCADStudio { list.into_iter().find(|(entry, _)| entry.eq_ignore_ascii_case(value)).map_or(Handle::NULL, |(_, h)| h) }; match setting { - "BACKGROUND" => view.background_handle = Handle::NULL, + "BACKGROUND" => replaced = std::mem::replace(&mut view.background_handle, Handle::NULL), "VISUALSTYLE" => view.visual_style_handle = find(crate::io::visual_styles::offered(document)), "SECTION" => { let sections: Vec<(String, Handle)> = document @@ -351,6 +416,7 @@ impl OpenCADStudio { _ => {} } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } } @@ -381,6 +447,8 @@ impl OpenCADStudio { image: String::new(), position: BgPosition::Center, aspect: false, + offset: [0.0, 0.0], + scale: [1.0, 1.0], canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8), }; let document = &self.tabs[i].scene.document; @@ -407,6 +475,8 @@ impl OpenCADStudio { BgPosition::Center }; state.aspect = value.maintain_aspect_ratio; + state.offset = [value.offset.x, value.offset.y]; + state.scale = [value.scale.x, value.scale.y]; } _ => {} } @@ -518,8 +588,8 @@ impl OpenCADStudio { fit_to_screen: state.position == BgPosition::Stretch, maintain_aspect_ratio: state.aspect, use_tiling: state.position == BgPosition::Tile, - offset: codec::types::Vector2::new(0.0, 0.0), - scale: codec::types::Vector2::new(1.0, 1.0), + offset: codec::types::Vector2::new(state.offset[0], state.offset[1]), + scale: codec::types::Vector2::new(state.scale[0], state.scale[1]), })) } }; @@ -533,7 +603,8 @@ impl OpenCADStudio { crate::scene::add_background(document, data) }); if let Some(view) = document.views.get_mut(&name) { - view.background_handle = handle; + let old = std::mem::replace(&mut view.background_handle, handle); + crate::scene::erase_background(document, old); } self.tabs[i].dirty = true; } @@ -549,7 +620,6 @@ impl OpenCADStudio { /// A restored view's visual style becomes the model tab's render mode /// (the closest of OCS's modes); a view without one leaves it. pub(crate) fn apply_view_render_mode(&mut self, i: usize, view: &codec::tables::View) { - use codec::entities::ViewportRenderMode as M; if self.tabs[i].scene.current_layout != "Model" { return; } @@ -558,19 +628,12 @@ impl OpenCADStudio { else { return; }; - let (mode, label) = match style.description.as_str() { - "2dWireframe" => (M::Wireframe2D, "Wireframe 2D"), - "Wireframe" => (M::Wireframe3D, "Wireframe 3D"), - "Hidden" => (M::HiddenLine, "Hidden Line"), - "Flat" => (M::FlatShaded, "Flat Shaded"), - "FlatWithEdges" => (M::FlatShadedWithEdges, "Flat Shaded + Edges"), - "GouraudWithEdges" | "Shaded with edges" => { - (M::GouraudShadedWithEdges, "Gouraud Shaded + Edges") - } - "Gouraud" | "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" - | "X-Ray" => (M::GouraudShaded, "Gouraud Shaded"), - _ => return, + use crate::modules::view::visual_style; + let Some((mode, named)) = visual_style::mode_for_style_name(&style.description) else { + return; }; - self.apply_render_mode(i, mode, label); + // A style of its own keeps its name; the render modes keep theirs. + let label = if named { style.description.clone() } else { visual_style::label_for(mode).to_string() }; + self.apply_render_mode(i, mode, &label); } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 2fdb1be62..57e627446 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1091,6 +1091,10 @@ impl OpenCADStudio { return Task::none(); } self.apply_render_mode(i, mode, label); + // The viewport record carries the style the mode is saved as. + self.tabs[i] + .scene + .set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode)); self.command_line .push_output(crate::tf!("Visual style: {label}").as_ref()); // The point cloud move gizmo follows the visual style. diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 200a1ffc3..31343993f 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -30,6 +30,7 @@ pub(super) fn viewport_controls<'a>( tile_count: usize, render_mode_menu_open: bool, render_mode_preview: Option, + style_name: Option, ) -> Element<'a, Message> { let render_modes: Vec = crate::modules::view::visual_style::VISUAL_STYLES .iter() @@ -98,7 +99,7 @@ pub(super) fn viewport_controls<'a>( // flyout keeps the current style unchanged while hovering; a click commits. let picker_head = button( row![ - text(RenderModeChoice(render_mode).to_string()).size(11), + text(style_name.unwrap_or_else(|| RenderModeChoice(render_mode).to_string())).size(11), crate::ui::icons::themed_arrow_toggle(render_mode_menu_open, 9.0), ] .spacing(6) @@ -379,7 +380,7 @@ mod tests { #[test] fn test_viewport_controls_construction() { for style in crate::modules::view::visual_style::VISUAL_STYLES { - let _el = viewport_controls(style.mode, true, true, true, 1, false, None); + let _el = viewport_controls(style.mode, true, true, true, 1, false, None, None); let _el_open = viewport_controls( style.mode, false, @@ -388,6 +389,7 @@ mod tests { 2, true, Some(M::Wireframe3D), + None, ); } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 933d30211..33841b9a3 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1297,6 +1297,18 @@ bg={bg_ms:.1}ms n={view_count}" let rect = tab.scene.active_model_tile_bounds(vw, vh); // Unified control chip: split buttons + render-mode picker + // grid / grid-snap toggles, for the active Model tile. + // A named style (Realistic, …) the tile draws as shows by name. + let style_name = match tab.scene.document.objects.get(&tab.scene.active_visual_style()) { + Some(codec::objects::ObjectType::VisualStyle(style)) => { + match crate::modules::view::visual_style::mode_for_style_name(&style.description) { + Some((mode, true)) if mode == tab.render_mode => { + Some(crate::t!(style.description.as_str()).into_owned()) + } + _ => None, + } + } + _ => None, + }; let bar = viewport_controls( tab.render_mode, self.show_grid, @@ -1305,6 +1317,7 @@ bg={bg_ms:.1}ms n={view_count}" tab.scene.model_tiles.borrow().len(), self.render_mode_menu_open, self.render_mode_preview, + style_name, ); // Adaptive: DensitySwap measures the bar's real width every frame // (reported into `render_bar_w`, which the ViewCube reads to decide @@ -1397,6 +1410,7 @@ bg={bg_ms:.1}ms n={view_count}" 0, self.render_mode_menu_open, self.render_mode_preview, + None, ); let adaptive: Element<'_, Message> = DensitySwap::new(vec![ iced::widget::opaque(bar), diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs index 4d3099f4f..a439e61ad 100644 --- a/src/modules/view/view_cmd.rs +++ b/src/modules/view/view_cmd.rs @@ -57,6 +57,8 @@ pub struct ViewInfo { pub category: String, pub visual_style: Option, pub live_section: Option, + /// Saved in paper space. + pub paper_space: bool, } /// What -VIEW checks typed names against. @@ -65,6 +67,10 @@ pub struct ViewNames { pub views: Vec, pub visual_styles: Vec, pub live_sections: Vec, + /// On a layout's sheet (no viewport entered): the layout's viewports as + /// (handle, [min x, min y, max x, max y]), one of which takes a + /// model-space view being restored. + pub sheet_viewports: Option>, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -106,6 +112,7 @@ enum Step { List, Delete, Restore, + RestoreViewport(String), Save, WindowName, WindowFirst(String), @@ -350,6 +357,7 @@ impl CadCommand for DashViewCommand { Step::List | Step::SettingViewList(_) => "Enter view name(s) to list <*>:".into(), Step::Delete => "Enter view name(s) to delete:".into(), Step::Restore => "Enter view name to restore:".into(), + Step::RestoreViewport(_) => "Select Viewport for view:".into(), Step::Save | Step::WindowName => "Enter view name to save:".into(), Step::WindowFirst(_) => "Specify first corner:".into(), Step::WindowSecond(..) => "Specify opposite corner:".into(), @@ -421,9 +429,19 @@ impl CadCommand for DashViewCommand { } } Step::Restore => match self.view(t) { + // On a sheet a model-space view goes into a viewport the + // user selects. + Some(view) if !view.paper_space && self.names.sheet_viewports.is_some() => { + self.step = Step::RestoreViewport(view.name.clone()); + CmdResult::ReportMeasurement("Restoring Model space view.".into()) + } Some(view) => CmdResult::Dispatch(format!("-VIEW >RESTORE {}", view.name)), None => Self::end(&format!("Cannot find view \"{t}\".")), }, + Step::RestoreViewport(name) => { + self.step = Step::RestoreViewport(name); + CmdResult::ReportError("*Invalid selection*".into()) + } Step::Save => CmdResult::Dispatch(format!("-VIEW >SAVE {t}")), Step::WindowName => { self.step = Step::WindowFirst(t.to_string()); @@ -494,6 +512,19 @@ impl CadCommand for DashViewCommand { "-VIEW >WINDOW {}\u{1f}{},{}\u{1f}{},{}", name, first.x, first.y, pt.x, pt.y )), + Step::RestoreViewport(name) => { + // The topmost viewport under the pick. + let hit = self.names.sheet_viewports.iter().flatten().rev().find(|(_, [x0, y0, x1, y1])| { + pt.x >= *x0 && pt.x <= *x1 && pt.y >= *y0 && pt.y <= *y1 + }); + match hit { + Some((handle, _)) => CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}")), + None => { + self.step = Step::RestoreViewport(name); + CmdResult::NeedPoint + } + } + } step => { self.step = step; CmdResult::NeedPoint @@ -503,7 +534,7 @@ impl CadCommand for DashViewCommand { fn on_enter(&mut self) -> CmdResult { match std::mem::replace(&mut self.step, Step::Main) { - Step::Main | Step::Delete | Step::Restore | Step::Save | Step::WindowName => { + Step::Main | Step::Delete | Step::Restore | Step::RestoreViewport(_) | Step::Save | Step::WindowName => { CmdResult::Dispatch("-VIEW >END".into()) } Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()), diff --git a/src/modules/view/visual_style.rs b/src/modules/view/visual_style.rs index f973038fc..9c999de89 100644 --- a/src/modules/view/visual_style.rs +++ b/src/modules/view/visual_style.rs @@ -106,6 +106,38 @@ pub fn label_for(mode: Mode) -> &'static str { .unwrap_or("Wireframe 2D") } +/// The render mode a named visual style draws as, and whether the name is a +/// style of its own (Realistic, Conceptual, …) rather than one of the modes. +pub fn mode_for_style_name(name: &str) -> Option<(Mode, bool)> { + Some(match name { + "2dWireframe" => (Mode::Wireframe2D, false), + "Wireframe" => (Mode::Wireframe3D, false), + "Hidden" => (Mode::HiddenLine, false), + "Flat" => (Mode::FlatShaded, false), + "FlatWithEdges" => (Mode::FlatShadedWithEdges, false), + "Gouraud" => (Mode::GouraudShaded, false), + "GouraudWithEdges" => (Mode::GouraudShadedWithEdges, false), + "Shaded with edges" => (Mode::GouraudShadedWithEdges, true), + "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" | "X-Ray" => { + (Mode::GouraudShaded, true) + } + _ => return None, + }) +} + +/// The name of the visual style a render mode is saved as. +pub fn style_name_for(mode: Mode) -> &'static str { + match mode { + Mode::Wireframe2D => "2dWireframe", + Mode::Wireframe3D => "Wireframe", + Mode::HiddenLine => "Hidden", + Mode::FlatShaded => "Flat", + Mode::FlatShadedWithEdges => "FlatWithEdges", + Mode::GouraudShaded => "Gouraud", + Mode::GouraudShadedWithEdges => "GouraudWithEdges", + } +} + /// The style a style keyword names. Only the seven exist; nothing maps onto a /// nearest neighbour, because there is no longer anything else to map from. pub fn mode_for_keyword(keyword: &str) -> Option { diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 0338d4970..e9dcbcc6b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -214,11 +214,28 @@ impl Scene { /// Restore camera to a named view from the document view table. pub fn restore_named_view(&mut self, view: &codec::tables::View) { - use glam::Vec3; - // The model viewport takes a copy of the view's background, or none. + let before = { + let cam = self.camera.borrow(); + (cam.rotation, cam.target, cam.distance) + }; + self.restore_named_view_camera(view); + let unchanged = { + let cam = self.camera.borrow(); + cam.rotation.abs_diff_eq(before.0, 1e-6) + && cam.target.abs_diff_eq(before.1, 1e-6 * before.1.length().max(1.0)) + && (cam.distance - before.2).abs() <= 1e-6 * before.2.abs().max(1.0) + }; + // The model viewport takes a copy of the view's background; a view + // without one clears it, unless the view is what the viewport already + // shows (the reference leaves the viewport as it is then). + if self.current_layout != "Model" && self.active_viewport.is_none() && view.paper_space { + self.set_sheet_background(view); + } if self.current_layout == "Model" { - let background = copy_background(&mut self.document, view.background_handle); - self.set_active_background(background); + if view.background_handle.is_valid() || !unchanged { + let background = copy_background(&mut self.document, view.background_handle); + self.set_active_background(background); + } if view.visual_style_handle.is_valid() { if let Some(handle) = self.active_tile_vport_handle() { if let Some(value) = self.document.vports.iter_mut().find(|v| v.handle == handle) { @@ -227,7 +244,50 @@ impl Scene { } } } - let cam = &mut *self.camera.borrow_mut(); + } + + fn restore_named_view_camera(&mut self, view: &codec::tables::View) { + named_view_camera(view, &mut self.camera.borrow_mut()); + self.camera_generation += 1; + } + + /// Restores a model-space view into a paper-space viewport: its camera, + /// a copy of the view's background (or none) and the view's visual style. + pub fn restore_named_view_in_viewport(&mut self, view: &codec::tables::View, viewport: Handle) { + let mut camera = self.navigation_camera(); + named_view_camera(view, &mut camera); + self.apply_navigation_camera(camera); + let background = copy_background(&mut self.document, view.background_handle); + let mut old = Handle::NULL; + if let Some(EntityType::Viewport(value)) = self.document.get_entity_mut(viewport) { + old = std::mem::replace(&mut value.background_handle, background); + if view.visual_style_handle.is_valid() { + value.visual_style_handle = view.visual_style_handle; + } + } + erase_background(&mut self.document, old); + self.notify_viewport_changed(viewport); + self.camera_generation += 1; + } + + /// The paper-space viewport of the current layout takes a copy of a + /// paper-space view's background (kept with the layout; the sheet itself + /// is not drawn over). + fn set_sheet_background(&mut self, view: &codec::tables::View) { + let sheet = self.current_layout_sheet_viewport_handle(); + let background = copy_background(&mut self.document, view.background_handle); + let old = match self.document.get_entity_mut(sheet) { + Some(EntityType::Viewport(value)) => std::mem::replace(&mut value.background_handle, background), + _ => background, + }; + erase_background(&mut self.document, old); + } +} + +/// Points a camera along a named view: direction, target and height. +fn named_view_camera(view: &codec::tables::View, cam: &mut view::camera::Camera) { + use glam::Vec3; + { // The stored direction points from the target toward the eye. let eye_dir = Vec3::new( view.direction.x as f32, @@ -254,22 +314,55 @@ impl Scene { } else { cam.distance }; - self.camera_generation += 1; } +} +impl Scene { /// The background the active model-space viewport shows (null for the /// drawing area colour). pub fn active_background(&self) -> Handle { + if self.current_layout != "Model" { + // Inside a viewport, that viewport's; the sheet's is not saved + // with a paper-space view. + return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) { + Some(EntityType::Viewport(value)) => value.background_handle, + _ => Handle::NULL, + }; + } self.active_tile_vport_handle() .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) .map_or(Handle::NULL, |value| value.background_handle) } + /// The visual style of the active Model tile's viewport record. + pub fn active_visual_style(&self) -> Handle { + self.active_tile_vport_handle() + .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) + .map_or(Handle::NULL, |value| value.visual_style_handle) + } + + /// Records the visual style named `name` on the active Model tile's + /// viewport record, when the drawing has it. + pub fn set_active_visual_style_named(&mut self, name: &str) { + let style = self.document.objects.iter().find_map(|(handle, object)| match object { + codec::objects::ObjectType::VisualStyle(style) if style.description == name => Some(*handle), + _ => None, + }); + let (Some(style), Some(handle)) = (style, self.active_tile_vport_handle()) else { return }; + if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { + value.visual_style_handle = style; + } + } + fn set_active_background(&mut self, background: Handle) { let Some(handle) = self.active_tile_vport_handle() else { return }; - if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { - value.background_handle = background; - self.camera_generation += 1; + let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) else { + return; + }; + let old = std::mem::replace(&mut value.background_handle, background); + self.camera_generation += 1; + if old != background { + erase_background(&mut self.document, old); } } @@ -1713,6 +1806,38 @@ pub fn add_background( handle } +/// Erases a background object once no view or viewport refers to it (the +/// reference erases a view's background when it is replaced, cleared or the +/// view deleted, and a viewport's when the viewport takes another). +pub fn erase_background(document: &mut codec::CadDocument, handle: Handle) { + use codec::objects::{ClassObjectData as D, ObjectType}; + if !handle.is_valid() + || document.views.iter().any(|view| view.background_handle == handle) + || document.vports.iter().any(|vport| vport.background_handle == handle) + || document.entities().any(|entity| { + matches!(entity, codec::EntityType::Viewport(viewport) if viewport.background_handle == handle) + }) + { + return; + } + let owner = match document.objects.get(&handle) { + Some(ObjectType::ClassObject(object)) + if matches!( + object.data, + D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_) + | D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_) + ) => + { + object.owner + } + _ => return, + }; + document.objects.remove(&handle); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&owner) { + entries.entries.retain(|(_, entry)| *entry != handle); + } +} + /// A copy of a background object (the reference gives each view and /// viewport its own); null for none. pub fn copy_background(document: &mut codec::CadDocument, handle: Handle) -> Handle { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0ebed3852..34f9804f3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -28,7 +28,7 @@ pub enum BlockScalePolicy { // blocks and/or free functions). Pure text-move from the original mod.rs. mod boundary; mod camera_ops; -pub use camera_ops::{add_background, copy_background}; +pub use camera_ops::{add_background, copy_background, erase_background}; pub(crate) mod centerline; pub(crate) mod centermark; pub(crate) mod dimension_assoc; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 297f5dffe..442f6415a 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -434,8 +434,10 @@ impl shader::Primitive for Primitive { // The scene works in logical pixels; an image background keeps // its size in device pixels. let mut uniforms = vp.uniforms; - uniforms.background_image_transform[2] /= scale.max(1e-3); - uniforms.background_image_transform[3] /= scale.max(1e-3); + // The image's size and offset are in device pixels. + for value in &mut uniforms.background_image_transform { + *value /= scale.max(1e-3); + } inner.upload_uniforms(device, queue, &uniforms); // ── Scene-render cache ──────────────────────────────────────── @@ -2557,20 +2559,18 @@ impl Scene { if viewport.paper_sheet { let handle = self.current_layout_sheet_viewport_handle(); if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) { - return build( - value.visual_style_handle, - value.brightness, - value.contrast, - value.background_handle, - ); + // The sheet keeps its page and desk: a paper-space view's + // background is kept with the layout but not drawn. + return build(value.visual_style_handle, value.brightness, value.contrast, Handle::NULL); } } else if viewport.handle.is_valid() { if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) { + let flat = value.render_mode == codec::entities::ViewportRenderMode::Wireframe2D; return build( value.visual_style_handle, value.brightness, value.contrast, - value.background_handle, + if flat { Handle::NULL } else { value.background_handle }, ); } } else if self.current_layout == "Model" { @@ -4907,7 +4907,11 @@ impl Scene { // (hatched) frame once it settles. Only applied to the on-screen // Model / paper content — the paper *sheet* keeps its fills. skip_hatch: self.hatch_lod_enabled() && navigating, - skip_background: !inst.paper_sheet && self.current_layout != "Model", + // A floating viewport shows the page behind it unless it has a + // background of its own. + skip_background: !inst.paper_sheet + && self.current_layout != "Model" + && display.background.params[0] < 0.5, geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, wire_content_id, diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 25a004c50..dd4f7671b 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -81,22 +81,25 @@ fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { fn image_uv(source_uv: vec2) -> vec2 { var uv = source_uv; if u.background_image_params.x < 0.5 { - // Not fitted: the image at its own pixel size (times its scale), - // centred on the viewport and moved by its offset. - let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)); - uv = pixel / max(abs(u.background_image_transform.zw), vec2(1e-6)) - + vec2(0.5) - u.background_image_transform.xy; + // Not fitted: the image at its own pixel size times its scale (a + // negative scale mirrors it), centred on the viewport and moved by + // its offset in pixels, right and down. + let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)) + - u.background_image_transform.xy; + let size = u.background_image_transform.zw; + let safe = select(size, vec2(1e-6), abs(size) < vec2(1e-6)); + uv = pixel / safe + vec2(0.5); return select(uv, fract(uv), u.background_image_params.z > 0.5); } + // Fitted: the image fills the viewport; keeping its aspect ratio it + // covers the viewport and the overflow is cut off evenly. if u.background_image_params.y > 0.5 { let image_aspect = max(u.background_image_params.w, 1e-6); let viewport_aspect = max(u.viewport_size.x / max(u.viewport_size.y, 1.0), 1e-6); if image_aspect > viewport_aspect { - let scale = viewport_aspect / image_aspect; - uv.y = (uv.y - 0.5) / scale + 0.5; + uv.x = (uv.x - 0.5) * (viewport_aspect / image_aspect) + 0.5; } else { - let scale = image_aspect / viewport_aspect; - uv.x = (uv.x - 0.5) / scale + 0.5; + uv.y = (uv.y - 0.5) * (image_aspect / viewport_aspect) + 0.5; } } if u.background_image_params.z > 0.5 { @@ -112,28 +115,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if mode == 1u { color = u.viewport_background.rgb; } else if mode == 2u { - // Bottom to top across the viewport, the "up" direction turned - // counter-clockwise by the rotation; in pixels, so the angle holds - // whatever the viewport's shape, and spanning corner to corner. + // Bottom to top along the "up" direction turned counter-clockwise by + // the rotation, in pixels from the viewport centre. As the reference + // draws it: the middle colour sits (horizon - 0.5) viewport widths + // from the centre and blends linearly into the top and bottom colours + // over height viewport heights on each side; two colours are one blend + // over a viewport height on each side of the centre. let angle = u.background_params.w; let up = vec2(-sin(angle), cos(angle)); let size = max(u.viewport_size, vec2(1.0)); let pixel = (vec2(in.uv.x, 1.0 - in.uv.y) - vec2(0.5)) * size; - let extent = max(abs(up.x) * size.x + abs(up.y) * size.y, 1.0); - let coordinate = clamp(dot(pixel, up) / extent + 0.5, 0.0, 1.0); - let horizon = clamp(u.background_params.y, 0.0, 1.0); - let half_height = abs(u.background_params.z) * 0.5; - if half_height <= 1e-6 { - // Two colours: one blend from the bottom to the top, over twice - // the viewport (the reference's two-colour gradient reaches the - // end colours a viewport beyond the edges). - color = mix(u.background_bottom.rgb, u.background_top.rgb, 0.25 + 0.5 * coordinate); - } else { - let lower = clamp((coordinate - (horizon - half_height)) / half_height, 0.0, 1.0); - let upper = clamp((coordinate - horizon) / half_height, 0.0, 1.0); - let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); - color = mix(low_color, u.background_top.rgb, upper); - } + let along = dot(pixel, up); + let three = abs(u.background_params.z) > 1e-6; + let middle_at = select(0.0, (u.background_params.y - 0.5) * size.x, three); + let half_band = select(1.0, abs(u.background_params.z), three) * size.y; + let middle = select(mix(u.background_bottom.rgb, u.background_top.rgb, 0.5), + u.background_middle.rgb, three); + let lower = clamp((along - middle_at + half_band) / half_band, 0.0, 1.0); + let upper = clamp((along - middle_at) / half_band, 0.0, 1.0); + color = mix(mix(u.background_bottom.rgb, middle, lower), u.background_top.rgb, upper); } else if mode == 3u { let direction = world_direction(in.ndc); if direction.z >= 0.0 { diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index a971b4081..cdf7cf97a 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -2206,6 +2206,9 @@ pub struct ViewBackgroundState { pub image: String, pub position: BgPosition, pub aspect: bool, + /// The image's offset (pixels) and scale, kept from the view's image. + pub offset: [f64; 2], + pub scale: [f64; 2], /// The drawing area colour, shown for no background. pub canvas: [u8; 3], }