feat(view): DVIEW twists the view about its line of sight

DVIEW (alias DV) offers TWist and Undo. TWist rolls the active view
or viewport camera to the typed angle; Undo steps back through the
twists made in the session. The new alias reaches existing profiles
through the alias migration table.

Refs #638

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 14:47:34 +03:00
1 parent 6490a625f2
commit 4ffae00199
31 files changed
+203 -1

No files matched your search

+2 -1
View File
@@ -80,7 +80,7 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES).
const DEFAULT_ALIASES_VERSION: u32 = 6;
const DEFAULT_ALIASES_VERSION: u32 = 7;
#[cfg(target_arch = "wasm32")]
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
@@ -101,6 +101,7 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
4 => &[("ER", "EXTERNALREFERENCES")],
5 => &[("-ATT", "-ATTDEF")],
6 => &[("VV", "QLAYER")],
7 => &[("DV", "DVIEW")],
_ => &[],
}
}
+3
View File
@@ -882,6 +882,9 @@ impl OpenCADStudio {
CmdResult::ZoomToWindow { p1, p2 } => {
self.handle_zoom_to_window(p1, p2);
}
CmdResult::TwistView(twist) => {
self.handle_twist_view(twist);
}
CmdResult::Measurement(msg) => {
self.handle_measurement(msg);
}
+20
View File
@@ -447,6 +447,26 @@ impl OpenCADStudio {
.push_output(crate::t!("Zoom Window").as_ref());
}
/// Turn the active view about its line of sight so the drawing shows
/// `twist` radians counter-clockwise from its untwisted orientation.
pub(in crate::app) fn handle_twist_view(&mut self, twist: f64) {
let i = self.active_tab;
self.tabs[i].scene.remember_current_view();
let set = |camera: &mut crate::scene::view::camera::Camera| {
// The drawing turns one way when the camera rolls the other.
camera.roll_by(-twist as f32 - camera.roll());
};
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i].scene.mutate_active_viewport_camera(set);
} else {
set(&mut self.tabs[i].scene.camera.borrow_mut());
}
self.tabs[i].scene.camera_generation += 1;
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
self.command_line.push_info(&prompt);
}
}
pub(super) fn handle_set_plot_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
let i = self.active_tab;
let layout_name = self.tabs[i].scene.current_layout.clone();
+15
View File
@@ -935,6 +935,21 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"DVIEW" => {
use crate::modules::view::dview::DviewCommand;
let twist = {
let scene = &self.tabs[i].scene;
let roll = match scene.active_viewport {
Some(_) => scene.navigation_camera().roll(),
None => scene.camera.borrow().roll(),
};
-(roll as f64)
};
let cmd = DviewCommand::new(twist.rem_euclid(std::f64::consts::TAU));
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"ZOOM" | "ZOOM WINDOW" | "ZOOM W" | "ZW" => {
use crate::modules::view::zoom_window::ZoomWindowCommand;
let new_cmd = ZoomWindowCommand::new();
+3
View File
@@ -1935,6 +1935,9 @@ pub enum CmdResult {
PasteClipboard {
base_pt: DVec3,
},
/// Set the view's twist about its line of sight (radians,
/// counter-clockwise); the command stays active.
TwistView(f64),
/// Zoom the model-space camera to fit the given corner points; end command.
ZoomToWindow {
p1: DVec3,
+3
View File
@@ -3838,6 +3838,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"ZOOM [Window / Extents / Extents All / All / In / Out / Scale]:" => Some(("view", "zoom-window-extents-extents-all-all-in-out-sca")),
"ZOOM scale factor (e.g. 2 or 0.5):" => Some(("annotate", "zoom-scale-factor-e-g-2-or-0-5")),
"ZOOM WINDOW Specify first corner:" => Some(("view", "zoom-window-specify-first-corner")),
"DVIEW Enter option [TWist/Undo]:" => Some(("view", "dview-enter-option-twist-undo")),
"DVIEW Specify view twist angle <{current}>:" => Some(("view", "dview-specify-view-twist-angle")),
"TWist" => Some(("view", "twist-option")),
"ZOOM WINDOW Specify opposite corner:" => Some(("view", "zoom-window-specify-opposite-corner")),
"Zero" => Some(("properties", "zero")),
"Zero suppress (DIMALTZ)" => Some(("common", "zero-suppress-dimaltz")),
+89
View File
@@ -0,0 +1,89 @@
// DVIEW command — turn the view about its line of sight (TWist).
use crate::t;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult};
pub struct DviewCommand {
/// The view's twist when the command started or last changed, radians.
current: f64,
/// Earlier twists, for Undo.
history: Vec<f64>,
twist_prompt: bool,
}
impl DviewCommand {
/// `current` is the view's present twist, counter-clockwise, in radians.
pub fn new(current: f64) -> Self {
Self {
current,
history: Vec::new(),
twist_prompt: false,
}
}
}
impl CadCommand for DviewCommand {
fn name(&self) -> &'static str {
"DVIEW"
}
fn prompt(&self) -> String {
if self.twist_prompt {
let current = crate::entities::common::format_angle(self.current);
crate::tf!("DVIEW Specify view twist angle <{current}>:").into_owned()
} else {
t!("DVIEW Enter option [TWist/Undo]:").into_owned()
}
}
fn options(&self) -> Vec<CmdOption> {
if self.twist_prompt {
return Vec::new();
}
vec![CmdOption::new("TWist", "TW"), CmdOption::new("Undo", "U")]
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let text = text.trim();
if self.twist_prompt {
let twist = crate::entities::common::parse_angle(&text.replace(',', "."))?;
self.twist_prompt = false;
self.history.push(self.current);
self.current = twist;
return Some(CmdResult::TwistView(twist));
}
match text.trim_start_matches('_').to_ascii_uppercase().as_str() {
"TW" | "TWIST" => {
self.twist_prompt = true;
Some(CmdResult::NeedPoint)
}
"U" | "UNDO" => {
let previous = self.history.pop()?;
self.current = previous;
Some(CmdResult::TwistView(previous))
}
_ => None,
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.twist_prompt {
// Enter keeps the current twist and returns to the options.
self.twist_prompt = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
}
inventory::submit!(crate::command::CommandRegistration { names: &["DVIEW"] });
+1
View File
@@ -5,6 +5,7 @@ mod count_palette;
mod file_tabs;
mod graphic_attributes_palette;
mod layout_tabs;
pub mod dview;
pub mod limits;
mod orbit;
mod ortho;