From 7c3cf7accc30887f25fdef7862af6ebf34ea4551 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 19:19:12 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Phase=201.2=20=E2=80=94=20VIEW=20comman?= =?UTF-8?q?d=20(save/restore/list/delete=20named=20views)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/app/commands.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/scene/mod.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/app/commands.rs b/src/app/commands.rs index 40f89cd57..c21112b4d 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1034,6 +1034,74 @@ impl H7CAD { } } + // ── Named Views (VIEW command) ──────────────────────────────── + cmd if cmd == "VIEW" || cmd.starts_with("VIEW ") => { + let parts: Vec<&str> = cmd.splitn(3, ' ').collect(); + let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default(); + match sub.as_str() { + "" | "LIST" | "?" => { + let views: Vec = self.tabs[i].scene.document + .views.iter().map(|v| v.name.clone()).collect(); + if views.is_empty() { + self.command_line.push_output("No named views saved."); + } else { + self.command_line.push_output(&format!( + "Named views: {}", views.join(", ") + )); + } + } + "SAVE" | "S" => { + let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: VIEW SAVE "); + } else { + let new_view = self.tabs[i].scene.current_as_named_view(&name); + self.tabs[i].scene.document.views.add_or_replace(new_view); + self.command_line.push_output(&format!("View '{}' saved.", name)); + } + } + "DELETE" | "DEL" | "D" => { + let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: VIEW DELETE "); + } else { + if self.tabs[i].scene.document.views.remove(&name).is_some() { + self.command_line.push_output(&format!("View '{}' deleted.", name)); + } else { + self.command_line.push_error(&format!("View '{}' not found.", name)); + } + } + } + "RESTORE" | "R" => { + let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: VIEW RESTORE "); + } else { + let found = self.tabs[i].scene.document.views.get(&name).cloned(); + if let Some(v) = found { + self.tabs[i].scene.restore_named_view(&v); + self.command_line.push_output(&format!("View '{}' restored.", v.name)); + } else { + self.command_line.push_error(&format!("View '{}' not found.", name)); + } + } + } + // VIEW shortcut for restore + _ => { + let name = sub.clone(); + let found = self.tabs[i].scene.document.views.get(&name).cloned(); + if let Some(v) = found { + self.tabs[i].scene.restore_named_view(&v); + self.command_line.push_output(&format!("View '{}' restored.", v.name)); + } else { + self.command_line.push_error( + "Usage: VIEW LIST | VIEW SAVE | VIEW RESTORE | VIEW DELETE " + ); + } + } + } + } + // ── Plot / Page Setup ────────────────────────────────────────── "PRINT"|"PLOT"|"EXPORT" => { return Task::done(Message::PlotExport); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ea1ac59eb..f9495cb0d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1618,6 +1618,75 @@ impl Scene { name.parse::().ok().map(Handle::new) } + /// Restore camera to a named view from the document view table. + pub fn restore_named_view(&mut self, view: &acadrust::tables::View) { + use glam::Vec3; + let cam = &mut *self.camera.borrow_mut(); + // view.target is the look-at point; view.direction is eye→target direction. + cam.target = Vec3::new(view.target.x as f32, view.target.y as f32, view.target.z as f32); + // direction in acadrust = from-target-to-eye (same as AutoCAD convention). + let eye_dir = Vec3::new( + view.direction.x as f32, + view.direction.y as f32, + view.direction.z as f32, + ); + let eye_dir = if eye_dir.length_squared() > 1e-10 { + eye_dir.normalize() + } else { + Vec3::Z + }; + // Build rotation: canonical eye is +Z, rotate to eye_dir. + cam.rotation = glam::Quat::from_rotation_arc(Vec3::Z, eye_dir); + // Sync yaw/pitch from new rotation (for ViewCube). + let pitch = eye_dir.z.clamp(-0.999, 0.999).asin(); + let yaw = eye_dir.x.atan2(eye_dir.y); + cam.yaw = yaw; + cam.pitch = pitch; + // Derive distance from view height and fov. + let h = view.height as f32; + cam.distance = if h > 0.0 { + h / (2.0 * (cam.fov_y * 0.5).tan()) + } else { + cam.distance + }; + self.camera_generation += 1; + } + + /// Save the current camera state into a new named view entry. + /// Returns the view; caller must push it into document.views. + pub fn current_as_named_view(&self, name: &str) -> acadrust::tables::View { + use acadrust::types::Vector3; + let cam = self.camera.borrow(); + let eye_dir = cam.rotation * glam::Vec3::Z; + let height = cam.ortho_size() * 2.0; + let width = height; // caller can adjust; rough square + acadrust::tables::View { + handle: acadrust::types::Handle::NULL, + name: name.to_string(), + center: Vector3 { + x: cam.target.x as f64, + y: cam.target.y as f64, + z: 0.0, + }, + target: Vector3 { + x: cam.target.x as f64, + y: cam.target.y as f64, + z: cam.target.z as f64, + }, + direction: Vector3 { + x: eye_dir.x as f64, + y: eye_dir.y as f64, + z: eye_dir.z as f64, + }, + height: height as f64, + width: width as f64, + lens_length: 50.0, + front_clip: 0.0, + back_clip: 0.0, + twist_angle: 0.0, + } + } + pub fn fit_all(&mut self) { let wires = self.entity_wires(); if wires.is_empty() {