feat(ucs): per-viewport UCS applied inside floating viewports
Extend UCS support to in-viewport (MSPACE) editing through the existing single-converter structure: one active_ucs drives every UCS-aware system, now in the Model tab AND inside a floating viewport. - Single predicate DocumentTab::editing_model_space() (Model tab or active viewport) replaces the model-only gate in ucs_wire_affine, ucs_rotation_angle, ucs_grid_basis, viewcube_ucs_mat, the coordinate readout and the cursor-plane pick. - Per-viewport UCS: refresh_active_ucs() sets active_ucs to the pane's UCS on every transition (the entered viewport's own UCS from its ucs_origin/x_axis/y_axis/ucs_per_viewport fields, the header model UCS in the Model tab, none in plain paper); persist_active_ucs() writes changes back to the viewport entity or the header so they round-trip. Wired into EnterViewport/ExitViewport, layout switch, load and the UCS command. - Visuals: grid_views now returns each pane's handle so every content viewport's grid aligns to its own UCS; the UCS icon draws inside the active viewport projected through the viewport camera. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a66d873e51
commit
295d3b1b5d
5 changed files with 164 additions and 33 deletions
|
|
@ -3834,8 +3834,11 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Keep the scene's ViewCube UCS in lock-step with active_ucs.
|
||||
// Keep the scene's ViewCube UCS in lock-step with active_ucs and
|
||||
// persist it to the active pane (per-viewport UCS inside a
|
||||
// viewport, header model UCS in the Model tab) so it round-trips.
|
||||
self.tabs[i].sync_ucs_to_scene();
|
||||
self.tabs[i].persist_active_ucs();
|
||||
}
|
||||
|
||||
// ── Named Views (VIEW command) ────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -171,24 +171,109 @@ impl DocumentTab {
|
|||
super::helpers::UcsXform::from_active(self.active_ucs.as_ref())
|
||||
}
|
||||
|
||||
/// Adopt the document's saved current UCS (the header's model-space UCS) as
|
||||
/// the active UCS, so the coordinate readout / icon / input follow the
|
||||
/// file's coordinate system the moment it opens. An identity UCS clears it
|
||||
/// back to plain WCS. Call wherever a document is loaded into the tab.
|
||||
pub(super) fn adopt_active_ucs_from_header(&mut self) {
|
||||
/// True when the active pane edits **model-space** geometry: the Model tab,
|
||||
/// or inside a floating viewport (MSPACE). The UCS applies in both; plain
|
||||
/// paper space (no active viewport) is excluded. Single predicate so every
|
||||
/// UCS-aware system shares one rule. [[feedback_shared_infra]]
|
||||
pub(super) fn editing_model_space(&self) -> bool {
|
||||
self.scene.current_layout == "Model" || self.scene.active_viewport.is_some()
|
||||
}
|
||||
|
||||
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
|
||||
/// is identity (plain WCS).
|
||||
fn model_ucs_from_header(&self) -> Option<Ucs> {
|
||||
let h = &self.scene.document.header;
|
||||
let mut u = Ucs::new(h.model_space_ucs_name.clone());
|
||||
u.origin = h.model_space_ucs_origin;
|
||||
u.x_axis = h.model_space_ucs_x_axis;
|
||||
u.y_axis = h.model_space_ucs_y_axis;
|
||||
self.active_ucs = if super::helpers::UcsXform::from_ucs(&u).is_identity() {
|
||||
if super::helpers::UcsXform::from_ucs(&u).is_identity() {
|
||||
None
|
||||
} else {
|
||||
Some(u)
|
||||
}
|
||||
}
|
||||
|
||||
/// A floating viewport's own per-viewport UCS, if it has one set. `None`
|
||||
/// when the viewport uses world coordinates or the handle is not a viewport.
|
||||
pub(super) fn ucs_from_viewport(&self, h: Handle) -> Option<Ucs> {
|
||||
let vp = match self.scene.document.get_entity(h) {
|
||||
Some(acadrust::EntityType::Viewport(vp)) => vp,
|
||||
_ => return None,
|
||||
};
|
||||
if !vp.ucs_per_viewport {
|
||||
return None;
|
||||
}
|
||||
let mut u = Ucs::new("*VPUCS*");
|
||||
u.origin = vp.ucs_origin;
|
||||
u.x_axis = vp.ucs_x_axis;
|
||||
u.y_axis = vp.ucs_y_axis;
|
||||
if super::helpers::UcsXform::from_ucs(&u).is_identity() {
|
||||
None
|
||||
} else {
|
||||
Some(u)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set `active_ucs` to the UCS of the *current pane*: the entered viewport's
|
||||
/// own per-viewport UCS, the model header UCS in the Model tab, or none in
|
||||
/// plain paper space. Keeps the ViewCube in lock-step. Call on every pane
|
||||
/// change (enter/exit viewport, layout / tab switch, load) so one field
|
||||
/// drives all UCS-aware systems regardless of where editing happens.
|
||||
pub(super) fn refresh_active_ucs(&mut self) {
|
||||
self.active_ucs = if let Some(h) = self.scene.active_viewport {
|
||||
self.ucs_from_viewport(h)
|
||||
} else if self.scene.current_layout == "Model" {
|
||||
self.model_ucs_from_header()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.sync_ucs_to_scene();
|
||||
}
|
||||
|
||||
/// Persist `active_ucs` back to its pane's storage so it round-trips: the
|
||||
/// entered viewport's per-viewport UCS fields, or the document header's
|
||||
/// model-space UCS in the Model tab. No-op in plain paper space. Call after
|
||||
/// any UCS change.
|
||||
pub(super) fn persist_active_ucs(&mut self) {
|
||||
use acadrust::types::Vector3;
|
||||
if let Some(h) = self.scene.active_viewport {
|
||||
let (o, x, y, per) = match &self.active_ucs {
|
||||
Some(u) => (u.origin, u.x_axis, u.y_axis, true),
|
||||
None => (Vector3::ZERO, Vector3::UNIT_X, Vector3::UNIT_Y, false),
|
||||
};
|
||||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
self.scene.document.get_entity_mut(h)
|
||||
{
|
||||
vp.ucs_origin = o;
|
||||
vp.ucs_x_axis = x;
|
||||
vp.ucs_y_axis = y;
|
||||
vp.ucs_per_viewport = per;
|
||||
}
|
||||
} else if self.scene.current_layout == "Model" {
|
||||
let h = &mut self.scene.document.header;
|
||||
match &self.active_ucs {
|
||||
Some(u) => {
|
||||
h.model_space_ucs_origin = u.origin;
|
||||
h.model_space_ucs_x_axis = u.x_axis;
|
||||
h.model_space_ucs_y_axis = u.y_axis;
|
||||
}
|
||||
None => {
|
||||
h.model_space_ucs_origin = Vector3::ZERO;
|
||||
h.model_space_ucs_x_axis = Vector3::UNIT_X;
|
||||
h.model_space_ucs_y_axis = Vector3::UNIT_Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adopt the active pane's UCS on load (the file's saved model-space UCS in
|
||||
/// the Model tab). Thin wrapper over [`refresh_active_ucs`] kept for the
|
||||
/// load call sites.
|
||||
pub(super) fn adopt_active_ucs_from_header(&mut self) {
|
||||
self.refresh_active_ucs();
|
||||
}
|
||||
|
||||
/// Push the active UCS rotation into the scene so the ViewCube composes with
|
||||
/// it. Call after any change to `active_ucs`.
|
||||
pub(super) fn sync_ucs_to_scene(&mut self) {
|
||||
|
|
@ -199,7 +284,7 @@ impl DocumentTab {
|
|||
/// geometry. Columns are the UCS axes; translation is the UCS origin in wire
|
||||
/// space. Identity outside model space (no UCS there).
|
||||
pub(super) fn ucs_wire_affine(&self) -> glam::Mat4 {
|
||||
if self.scene.current_layout != "Model" {
|
||||
if !self.editing_model_space() {
|
||||
return glam::Mat4::IDENTITY;
|
||||
}
|
||||
let (o, x, y, z) = self.ucs_xform().axes();
|
||||
|
|
@ -221,7 +306,7 @@ impl DocumentTab {
|
|||
/// default rotation for new text-bearing objects so their text aligns to
|
||||
/// the user's coordinate system. Zero outside model space / with no UCS.
|
||||
pub(super) fn ucs_rotation_angle(&self) -> f64 {
|
||||
if self.scene.current_layout != "Model" {
|
||||
if !self.editing_model_space() {
|
||||
return 0.0;
|
||||
}
|
||||
let (_, x, ..) = self.ucs_xform().axes();
|
||||
|
|
@ -231,7 +316,7 @@ impl DocumentTab {
|
|||
/// Grid origin (render/wire space) and UCS→world rotation for grid snap and
|
||||
/// the grid overlay. Identity / origin-at-zero outside model space.
|
||||
pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) {
|
||||
if self.scene.current_layout != "Model" {
|
||||
if !self.editing_model_space() {
|
||||
return (glam::Vec3::ZERO, glam::Mat4::IDENTITY);
|
||||
}
|
||||
let xf = self.ucs_xform();
|
||||
|
|
|
|||
|
|
@ -5688,6 +5688,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.active_viewport = None;
|
||||
self.tabs[i].scene.set_current_layout(name);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
// UCS follows the pane: model header UCS in the Model tab, none
|
||||
// in plain paper space (a viewport's UCS is adopted on entry).
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
// Grid/snap are per-view: load the layout we just entered (its
|
||||
|
|
@ -6213,6 +6216,8 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.normalize_active_viewport_view();
|
||||
// Grid/snap follow the entered viewport.
|
||||
self.adopt_view_display(i);
|
||||
// Adopt the entered viewport's own per-viewport UCS.
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output("MSPACE");
|
||||
Task::none()
|
||||
|
|
@ -6225,6 +6230,8 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.active_viewport = None;
|
||||
// Grid/snap return to the paper sheet's own state.
|
||||
self.adopt_view_display(i);
|
||||
// Paper space has no UCS — drop the viewport's UCS.
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output("PSPACE");
|
||||
Task::none()
|
||||
|
|
@ -8323,12 +8330,19 @@ impl OpenCADStudio {
|
|||
p: iced::Point,
|
||||
bounds: iced::Rectangle,
|
||||
) -> glam::DVec3 {
|
||||
let plane = self.tabs[i].active_ucs.as_ref().map(|ucs| {
|
||||
(
|
||||
ucs_z_axis(ucs),
|
||||
glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z),
|
||||
)
|
||||
});
|
||||
// Constrain to the UCS plane only where the UCS applies (model space or
|
||||
// inside a viewport); plain paper space uses the target plane.
|
||||
let plane = self
|
||||
.tabs[i]
|
||||
.active_ucs
|
||||
.as_ref()
|
||||
.filter(|_| self.tabs[i].editing_model_space())
|
||||
.map(|ucs| {
|
||||
(
|
||||
ucs_z_axis(ucs),
|
||||
glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z),
|
||||
)
|
||||
});
|
||||
let pick = |cam: &crate::scene::view::camera::Camera| match plane {
|
||||
Some((normal, origin)) => cam.pick_on_plane(p, bounds, normal, origin),
|
||||
None => cam.pick_on_target_plane(p, bounds),
|
||||
|
|
|
|||
|
|
@ -155,11 +155,10 @@ impl OpenCADStudio {
|
|||
// stay independent: model tiles in model space; the sheet plus each
|
||||
// floating viewport (clipped to its rectangle) in paper space. One
|
||||
// enumeration for both, shared with the renderer.
|
||||
// Align the model grid to the active UCS (origin in wire space, world
|
||||
// axis directions); paper space stays plain WCS.
|
||||
let (grid_origin, grid_axes): (glam::DVec3, _) = if is_paper {
|
||||
(glam::DVec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z))
|
||||
} else {
|
||||
// Align each grid to its pane's UCS (origin in wire space, world
|
||||
// axis directions): model tiles to the tab's model UCS, each content
|
||||
// viewport to its own per-viewport UCS, the paper sheet to plain WCS.
|
||||
let model_basis = {
|
||||
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
(o.as_dvec3(), (ux, uy, uz))
|
||||
};
|
||||
|
|
@ -167,27 +166,53 @@ impl OpenCADStudio {
|
|||
.scene
|
||||
.grid_views(vw, vh)
|
||||
.into_iter()
|
||||
.map(|(bounds, cam)| {
|
||||
.map(|(bounds, cam, handle)| {
|
||||
let (origin, axes): (glam::DVec3, _) = if is_paper {
|
||||
match tab.ucs_from_viewport(handle) {
|
||||
Some(u) => {
|
||||
let (o, ux, uy, uz) =
|
||||
super::helpers::UcsXform::from_ucs(&u).axes();
|
||||
(o.as_dvec3(), (ux, uy, uz))
|
||||
}
|
||||
None => (
|
||||
glam::DVec3::ZERO,
|
||||
(glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
model_basis
|
||||
};
|
||||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||||
overlay::GridParams {
|
||||
view_rot: cam.view_proj_rte(bounds),
|
||||
eye: cam.eye_f64(),
|
||||
bounds,
|
||||
plane,
|
||||
origin: grid_origin,
|
||||
axes: grid_axes,
|
||||
origin,
|
||||
axes,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
||||
// The UCS icon shows the active pane's UCS tripod: the model view, or
|
||||
// (inside a floating viewport) projected through the viewport camera
|
||||
// at the viewport's rect so it tracks the in-viewport UCS.
|
||||
// Rotation-only projection (view_proj_rte): the icon shows axis
|
||||
// DIRECTIONS only, so the full view_proj's huge UTM translation would
|
||||
// cancel catastrophically in f32 and make the tripod jitter.
|
||||
let ucs_icon = if !self.show_ucs_icon {
|
||||
None
|
||||
} else if let Some((vp_cam, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
|
||||
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
Some(overlay::UcsIconParams {
|
||||
view_proj: vp_cam.view_proj_rte(full),
|
||||
bounds: full,
|
||||
axes: (ux, uy, uz),
|
||||
})
|
||||
} else if !is_paper {
|
||||
let cam = tab.scene.camera.borrow();
|
||||
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
Some(overlay::UcsIconParams {
|
||||
// Rotation-only projection: the icon shows axis DIRECTIONS
|
||||
// only, so the full view_proj's huge UTM translation would
|
||||
// just cancel catastrophically in f32 and make the tripod
|
||||
// jitter. view_proj_rte drops the translation. (#utm-ucs)
|
||||
view_proj: cam.view_proj_rte(vp_bounds),
|
||||
bounds: vp_bounds,
|
||||
axes: (ux, uy, uz),
|
||||
|
|
@ -916,7 +941,9 @@ impl OpenCADStudio {
|
|||
// user's coordinate system, not raw WCS (no-op without UCS).
|
||||
let cursor_coord = {
|
||||
let lc = tab.last_cursor_world;
|
||||
if is_model {
|
||||
// The readout follows the active pane's UCS — model space
|
||||
// or inside a floating viewport (no-op without a UCS).
|
||||
if tab.editing_model_space() {
|
||||
let wo = [0.0_f64; 3];
|
||||
let wcs = glam::Vec3::new(
|
||||
lc.x + wo[0] as f32,
|
||||
|
|
|
|||
|
|
@ -3967,7 +3967,9 @@ impl Scene {
|
|||
/// the active UCS in model space, identity everywhere else. Render,
|
||||
/// hit-test, and click-snap all go through this so they stay in lock-step.
|
||||
pub fn viewcube_ucs_mat(&self) -> glam::Mat4 {
|
||||
if self.current_layout == "Model" {
|
||||
// UCS applies in model space and inside a floating viewport (MSPACE);
|
||||
// plain paper space stays WCS.
|
||||
if self.current_layout == "Model" || self.active_viewport.is_some() {
|
||||
self.viewcube_ucs
|
||||
} else {
|
||||
glam::Mat4::IDENTITY
|
||||
|
|
@ -7366,11 +7368,11 @@ impl Scene {
|
|||
/// `active_viewports` enumeration the renderer uses, so the grid overlay can
|
||||
/// never drift from the views actually on screen (issue #121). The grid
|
||||
/// ignores render mode, so any value passes through.
|
||||
pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera)> {
|
||||
pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera, Handle)> {
|
||||
self.active_viewports(vw, vh, acadrust::entities::ViewportRenderMode::Wireframe2D)
|
||||
.into_iter()
|
||||
.filter(|inst| inst.grid_on)
|
||||
.map(|inst| (inst.screen_rect, inst.camera))
|
||||
.map(|inst| (inst.screen_rect, inst.camera, inst.handle))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue