Feat: Phase 1.2 — VIEW command (save/restore/list/delete named views)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7a5e3ed6ed
commit
7c3cf7accc
2 changed files with 137 additions and 0 deletions
|
|
@ -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<String> = 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 <name>");
|
||||||
|
} 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 <name>");
|
||||||
|
} 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 <name>");
|
||||||
|
} 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 <name> 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 <name> | VIEW RESTORE <name> | VIEW DELETE <name>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Plot / Page Setup ──────────────────────────────────────────
|
// ── Plot / Page Setup ──────────────────────────────────────────
|
||||||
"PRINT"|"PLOT"|"EXPORT" => {
|
"PRINT"|"PLOT"|"EXPORT" => {
|
||||||
return Task::done(Message::PlotExport);
|
return Task::done(Message::PlotExport);
|
||||||
|
|
|
||||||
|
|
@ -1618,6 +1618,75 @@ impl Scene {
|
||||||
name.parse::<u64>().ok().map(Handle::new)
|
name.parse::<u64>().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) {
|
pub fn fit_all(&mut self) {
|
||||||
let wires = self.entity_wires();
|
let wires = self.entity_wires();
|
||||||
if wires.is_empty() {
|
if wires.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue