forked from 1b-agent/opencadstudio-upstream
feat: restore and persist saved camera position per layout
- On file open and layout tab switch, restore the camera from the document's saved view (*Active VPort for model space, sheet viewport for paper space) instead of fitting to wire bounds. - After any zoom/pan/rotate, write the new camera position back to the document and mark the file dirty so it is saved with the file. - Uses last_synced_camera_gen to avoid marking the file dirty on the initial camera restore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed94a5b81f
commit
8f90a68930
3 files changed
+211
-3
No files matched your search
@@ -42,6 +42,8 @@ pub(super) struct DocumentTab {
|
||||
pub(super) refedit_session: Option<RefEditSession>,
|
||||
/// Currently active MLeader style name.
|
||||
pub(super) active_mleader_style: String,
|
||||
/// Last camera_generation value written back to the document.
|
||||
pub(super) last_synced_camera_gen: u64,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
@@ -83,6 +85,7 @@ impl DocumentTab {
|
||||
paper_bg_color: None,
|
||||
refedit_session: None,
|
||||
active_mleader_style: "Standard".to_string(),
|
||||
last_synced_camera_gen: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -17,7 +17,19 @@ impl H7CAD {
|
||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
match msg {
|
||||
Message::Tick(t) => {
|
||||
self.tabs[self.active_tab].scene.update(t - self.start);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.update(t - self.start);
|
||||
|
||||
// If the camera moved since we last synced, write it back to
|
||||
// the document and mark the file dirty.
|
||||
let gen = self.tabs[i].scene.camera_generation;
|
||||
if gen != self.tabs[i].last_synced_camera_gen {
|
||||
self.tabs[i].last_synced_camera_gen = gen;
|
||||
if self.tabs[i].scene.sync_camera_to_document() {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -86,7 +98,8 @@ impl H7CAD {
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.sync_ribbon_layers();
|
||||
self.tabs[i].scene.fit_all();
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
self.tabs[i].dirty = false;
|
||||
self.tabs[i].history = super::document::HistoryState::default();
|
||||
self.refresh_selected_grips();
|
||||
@@ -2249,7 +2262,8 @@ impl H7CAD {
|
||||
self.tabs[i].scene.active_viewport = None;
|
||||
self.tabs[i].scene.current_layout = name;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.fit_all();
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||
if going_to_paper {
|
||||
if let Some(idx) = self.ribbon.layout_module_index() {
|
||||
self.ribbon.select(idx);
|
||||
|
||||
@@ -2380,6 +2380,197 @@ impl Scene {
|
||||
self.camera_generation += 1;
|
||||
}
|
||||
|
||||
/// Set the model-space camera from the VPORT table's *Active entry.
|
||||
/// Returns true if the entry was found and the camera was set.
|
||||
fn apply_active_vport_camera(&mut self) -> bool {
|
||||
let vp = match self.document.vports.iter().find(|v| v.name == "*Active") {
|
||||
Some(v) => v.clone(),
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if vp.view_height.abs() < 1e-9 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let vd = glam::Vec3::new(
|
||||
vp.view_direction.x as f32,
|
||||
vp.view_direction.y as f32,
|
||||
vp.view_direction.z as f32,
|
||||
)
|
||||
.normalize_or(glam::Vec3::Z);
|
||||
|
||||
let pitch = vd.z.clamp(-0.999, 0.999).asin();
|
||||
let yaw = vd.x.atan2(vd.y);
|
||||
let rotation = camera::yaw_pitch_to_quat(yaw, pitch);
|
||||
let view_right = rotation * glam::Vec3::X;
|
||||
let view_up = rotation * glam::Vec3::Y;
|
||||
|
||||
// view_target is WCS; wire-space subtracts world_offset.
|
||||
let base = glam::Vec3::new(
|
||||
(vp.view_target.x - self.world_offset[0]) as f32,
|
||||
(vp.view_target.y - self.world_offset[1]) as f32,
|
||||
(vp.view_target.z - self.world_offset[2]) as f32,
|
||||
);
|
||||
let target = base
|
||||
+ view_right * vp.view_center.x as f32
|
||||
+ view_up * vp.view_center.y as f32;
|
||||
|
||||
let fov_y = 45.0_f32.to_radians();
|
||||
let distance = ((vp.view_height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001);
|
||||
|
||||
let mut cam = self.camera.borrow_mut();
|
||||
cam.target = target;
|
||||
cam.rotation = rotation;
|
||||
cam.distance = distance;
|
||||
cam.yaw = yaw;
|
||||
cam.pitch = pitch;
|
||||
cam.fov_y = fov_y;
|
||||
cam.projection = camera::Projection::Orthographic;
|
||||
drop(cam);
|
||||
|
||||
self.camera_generation += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Set the paper-space camera from the sheet viewport's stored view.
|
||||
/// Returns true if a valid sheet viewport was found and the camera was set.
|
||||
fn apply_sheet_viewport_camera(&mut self) -> bool {
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
if layout_block.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let sheet_vp = self.document.entities()
|
||||
.filter_map(|e| if let EntityType::Viewport(vp) = e { Some(vp) } else { None })
|
||||
.find(|vp| {
|
||||
vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp)
|
||||
});
|
||||
|
||||
let vp = match sheet_vp {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if vp.view_height.abs() < 1e-9 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let vd = glam::Vec3::new(
|
||||
vp.view_direction.x as f32,
|
||||
vp.view_direction.y as f32,
|
||||
vp.view_direction.z as f32,
|
||||
)
|
||||
.normalize_or(glam::Vec3::Z);
|
||||
|
||||
let pitch = vd.z.clamp(-0.999, 0.999).asin();
|
||||
let yaw = vd.x.atan2(vd.y);
|
||||
let rotation = camera::yaw_pitch_to_quat(yaw, pitch);
|
||||
let view_right = rotation * glam::Vec3::X;
|
||||
let view_up = rotation * glam::Vec3::Y;
|
||||
|
||||
// Paper-space entities have no world_offset applied, so target is raw.
|
||||
let base = glam::Vec3::new(
|
||||
vp.view_target.x as f32,
|
||||
vp.view_target.y as f32,
|
||||
vp.view_target.z as f32,
|
||||
);
|
||||
let target = base
|
||||
+ view_right * vp.view_center.x as f32
|
||||
+ view_up * vp.view_center.y as f32;
|
||||
|
||||
let fov_y = 45.0_f32.to_radians();
|
||||
let distance = ((vp.view_height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001);
|
||||
|
||||
let mut cam = self.camera.borrow_mut();
|
||||
cam.target = target;
|
||||
cam.rotation = rotation;
|
||||
cam.distance = distance;
|
||||
cam.yaw = yaw;
|
||||
cam.pitch = pitch;
|
||||
cam.fov_y = fov_y;
|
||||
cam.projection = camera::Projection::Orthographic;
|
||||
drop(cam);
|
||||
|
||||
self.camera_generation += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Write the current camera back into the document (VPort or sheet viewport)
|
||||
/// so it is saved with the file. Returns true if the document was modified.
|
||||
pub fn sync_camera_to_document(&mut self) -> bool {
|
||||
let cam = self.camera.borrow().clone();
|
||||
let view_dir = cam.rotation * glam::Vec3::Z;
|
||||
let view_height = cam.ortho_size() * 2.0;
|
||||
|
||||
if self.current_layout == "Model" {
|
||||
// Write back to the *Active VPort entry.
|
||||
let vp = match self.document.vports.iter_mut().find(|v| v.name == "*Active") {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
// cam.target is in wire-space; convert back to raw model WCS.
|
||||
vp.view_target = acadrust::types::Vector3 {
|
||||
x: (cam.target.x as f64) + self.world_offset[0],
|
||||
y: (cam.target.y as f64) + self.world_offset[1],
|
||||
z: (cam.target.z as f64) + self.world_offset[2],
|
||||
};
|
||||
vp.view_center = acadrust::types::Vector2::ZERO;
|
||||
vp.view_direction = acadrust::types::Vector3 {
|
||||
x: view_dir.x as f64,
|
||||
y: view_dir.y as f64,
|
||||
z: view_dir.z as f64,
|
||||
};
|
||||
vp.view_height = view_height as f64;
|
||||
true
|
||||
} else {
|
||||
// Write back to the sheet viewport entity.
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
if layout_block.is_null() { return false; }
|
||||
|
||||
let sheet_handle = self.document.entities()
|
||||
.filter_map(|e| if let EntityType::Viewport(vp) = e { Some(vp) } else { None })
|
||||
.find(|vp| vp.common.owner_handle == layout_block && !Self::is_content_viewport(vp))
|
||||
.map(|vp| vp.common.handle);
|
||||
|
||||
let handle = match sheet_handle {
|
||||
Some(h) => h,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
||||
// Paper-space entities have no world_offset; target is raw paper coords.
|
||||
vp.view_target = acadrust::types::Vector3 {
|
||||
x: cam.target.x as f64,
|
||||
y: cam.target.y as f64,
|
||||
z: cam.target.z as f64,
|
||||
};
|
||||
vp.view_center = acadrust::types::Vector3::ZERO;
|
||||
vp.view_direction = acadrust::types::Vector3 {
|
||||
x: view_dir.x as f64,
|
||||
y: view_dir.y as f64,
|
||||
z: view_dir.z as f64,
|
||||
};
|
||||
vp.view_height = view_height as f64;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the camera from the file's saved view (called once on open).
|
||||
/// Falls back to fit_all() if no saved view is available.
|
||||
pub fn restore_saved_camera(&mut self) {
|
||||
let restored = if self.current_layout == "Model" {
|
||||
self.apply_active_vport_camera()
|
||||
} else {
|
||||
self.apply_sheet_viewport_camera()
|
||||
};
|
||||
if !restored {
|
||||
self.fit_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fit_all(&mut self) {
|
||||
let wires = self.entity_wires();
|
||||
if wires.is_empty() {
|
||||
|
||||
Reference in new issue
Block a user