forked from 1b-agent/opencadstudio-upstream
feat(layout): MVSETUP sets a drawing up for plotting
In model space MVSETUP draws the sheet border at the drawing scale (scale factor, paper width and height) and zooms to it, or switches to a layout. On a layout it creates viewports in a bounding area (a single one, a 2x2 engineering set of top/front/right/isometric views, or an array with spacing), deletes picked objects, and scales picked viewports to a paper/model unit ratio. A viewport's fit to the model now measures the model along the viewport's own view direction, so side and isometric views frame it too. Refs #1501 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1d4c9bd9b
commit
57eb7fe4db
31 files changed
+929
-10
No files matched your search
@@ -2,6 +2,7 @@
|
||||
// This tab is only shown when the active layout is not "Model".
|
||||
|
||||
pub mod mview;
|
||||
pub mod mvsetup;
|
||||
pub mod vplayer;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, ToolDef};
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// MVSETUP — set a drawing up for plotting.
|
||||
//
|
||||
// In model space without paper space it draws the sheet border at the
|
||||
// drawing scale:
|
||||
// Enable paper space? [No/Yes] <Y>:
|
||||
// Enter the scale factor <1>:
|
||||
// Enter the paper width:
|
||||
// Enter the paper height:
|
||||
// On a layout it lays out viewports or scales them:
|
||||
// Enter an option [Create/Delete objects/Scale viewports]:
|
||||
// Enter viewport layout [Single/Engineering/Array] <Single>:
|
||||
// Specify first corner of bounding area for viewport(s) <Sheet>:
|
||||
// Specify opposite corner:
|
||||
// Enter number of viewports in X direction <2>: (Array)
|
||||
// Enter number of viewports in Y direction <2>: (Array)
|
||||
// Specify distance between viewports in X direction <0>:
|
||||
// Specify distance between viewports in Y direction <0>:
|
||||
// Select the objects to delete:
|
||||
// Select the viewports to scale:
|
||||
// Enter the number of paper space units <1>:
|
||||
// Enter the number of model space units <1>:
|
||||
|
||||
use crate::t;
|
||||
use codec::entities::{LwPolyline, LwVertex, Viewport};
|
||||
use codec::types::{Vector2, Vector3};
|
||||
use codec::{EntityType, Handle};
|
||||
use glam::{DVec2, DVec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Step {
|
||||
PaperSpace,
|
||||
Scale,
|
||||
Width,
|
||||
Height,
|
||||
Option,
|
||||
Arrangement,
|
||||
FirstCorner,
|
||||
SecondCorner,
|
||||
Columns,
|
||||
Rows,
|
||||
GapX,
|
||||
GapY,
|
||||
Delete,
|
||||
Viewports,
|
||||
PaperUnits,
|
||||
ModelUnits,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Arrangement {
|
||||
Single,
|
||||
Engineering,
|
||||
Array,
|
||||
}
|
||||
|
||||
pub struct MvsetupCommand {
|
||||
step: Step,
|
||||
/// The layout model space switches to when paper space is enabled.
|
||||
layout: String,
|
||||
paper_bounds: ((f64, f64), (f64, f64)),
|
||||
scale: f64,
|
||||
width: f64,
|
||||
arrangement: Arrangement,
|
||||
first: DVec2,
|
||||
area: (DVec2, DVec2),
|
||||
columns: u32,
|
||||
rows: u32,
|
||||
gap_x: f64,
|
||||
picked: Vec<Handle>,
|
||||
paper_units: f64,
|
||||
}
|
||||
|
||||
impl MvsetupCommand {
|
||||
/// `layout` is `None` on a layout, or the layout to switch to from model
|
||||
/// space; `paper_bounds` is the sheet's printable area.
|
||||
pub fn new(layout: Option<String>, paper_bounds: ((f64, f64), (f64, f64))) -> Self {
|
||||
Self {
|
||||
step: if layout.is_some() {
|
||||
Step::PaperSpace
|
||||
} else {
|
||||
Step::Option
|
||||
},
|
||||
layout: layout.unwrap_or_default(),
|
||||
paper_bounds,
|
||||
scale: 1.0,
|
||||
width: 0.0,
|
||||
arrangement: Arrangement::Single,
|
||||
first: DVec2::ZERO,
|
||||
area: (DVec2::ZERO, DVec2::ZERO),
|
||||
columns: 2,
|
||||
rows: 2,
|
||||
gap_x: 0.0,
|
||||
picked: Vec::new(),
|
||||
paper_units: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn after_area(&mut self) -> CmdResult {
|
||||
self.step = match self.arrangement {
|
||||
Arrangement::Single => return self.create(0.0),
|
||||
Arrangement::Engineering => Step::GapX,
|
||||
Arrangement::Array => Step::Columns,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
/// The viewports of the chosen arrangement, `gap_y` apart vertically.
|
||||
fn create(&self, gap_y: f64) -> CmdResult {
|
||||
let (columns, rows, gap_x) = match self.arrangement {
|
||||
Arrangement::Single => (1, 1, 0.0),
|
||||
Arrangement::Engineering => (2, 2, self.gap_x),
|
||||
Arrangement::Array => (self.columns, self.rows, self.gap_x),
|
||||
};
|
||||
let (min, max) = self.area;
|
||||
let width = (max.x - min.x - gap_x * (columns - 1) as f64) / columns as f64;
|
||||
let height = (max.y - min.y - gap_y * (rows - 1) as f64) / rows as f64;
|
||||
if width <= 1e-6 || height <= 1e-6 {
|
||||
return CmdResult::ReportError(
|
||||
t!("MVSETUP The viewports do not fit in the bounding area.").into_owned(),
|
||||
);
|
||||
}
|
||||
// Engineering: top view over front view, right view beside it, and
|
||||
// an isometric view in the remaining corner.
|
||||
let directions = [
|
||||
Vector3::new(0.0, 0.0, 1.0),
|
||||
Vector3::new(1.0, -1.0, 1.0),
|
||||
Vector3::new(0.0, -1.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
];
|
||||
let mut viewports = Vec::new();
|
||||
for row in 0..rows {
|
||||
for column in 0..columns {
|
||||
let mut viewport = Viewport::new();
|
||||
viewport.center = Vector3::new(
|
||||
min.x + column as f64 * (width + gap_x) + width / 2.0,
|
||||
max.y - row as f64 * (height + gap_y) - height / 2.0,
|
||||
0.0,
|
||||
);
|
||||
viewport.width = width;
|
||||
viewport.height = height;
|
||||
viewport.id = 2;
|
||||
if self.arrangement == Arrangement::Engineering {
|
||||
viewport.view_direction = directions[(row * 2 + column) as usize].clone();
|
||||
}
|
||||
viewports.push(EntityType::Viewport(viewport));
|
||||
}
|
||||
}
|
||||
CmdResult::CommitEntitiesAndExit(viewports)
|
||||
}
|
||||
|
||||
fn sheet_border(&self, height: f64) -> CmdResult {
|
||||
let (width, height) = (self.width * self.scale, height * self.scale);
|
||||
let mut border = LwPolyline::new();
|
||||
border.is_closed = true;
|
||||
border.vertices = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)]
|
||||
.into_iter()
|
||||
.map(|(x, y)| LwVertex::new(Vector2::new(x, y)))
|
||||
.collect();
|
||||
CmdResult::CommitAndZoom {
|
||||
entity: EntityType::LwPolyline(border),
|
||||
p1: DVec3::ZERO,
|
||||
p2: DVec3::new(width, height, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A positive number, or `default` for an empty answer.
|
||||
fn positive(text: &str, default: f64) -> Option<f64> {
|
||||
if text.is_empty() {
|
||||
return Some(default);
|
||||
}
|
||||
crate::entities::common::parse_typed_length(text).filter(|value| *value > 0.0)
|
||||
}
|
||||
|
||||
impl CadCommand for MvsetupCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"MVSETUP"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
t!(match self.step {
|
||||
Step::PaperSpace => "MVSETUP Enable paper space? [No/Yes] <Y>:",
|
||||
Step::Scale => "MVSETUP Enter the scale factor <1>:",
|
||||
Step::Width => "MVSETUP Enter the paper width:",
|
||||
Step::Height => "MVSETUP Enter the paper height:",
|
||||
Step::Option => "MVSETUP Enter an option [Create/Delete objects/Scale viewports]:",
|
||||
Step::Arrangement => {
|
||||
"MVSETUP Enter viewport layout [Single/Engineering/Array] <Single>:"
|
||||
}
|
||||
Step::FirstCorner => {
|
||||
"MVSETUP Specify first corner of bounding area for viewport(s) <Sheet>:"
|
||||
}
|
||||
Step::SecondCorner => "MVSETUP Specify opposite corner:",
|
||||
Step::Columns => "MVSETUP Enter number of viewports in X direction <2>:",
|
||||
Step::Rows => "MVSETUP Enter number of viewports in Y direction <2>:",
|
||||
Step::GapX => "MVSETUP Specify distance between viewports in X direction <0>:",
|
||||
Step::GapY => "MVSETUP Specify distance between viewports in Y direction <0>:",
|
||||
Step::Delete => "MVSETUP Select the objects to delete:",
|
||||
Step::Viewports => "MVSETUP Select the viewports to scale:",
|
||||
Step::PaperUnits => "MVSETUP Enter the number of paper space units <1>:",
|
||||
Step::ModelUnits => "MVSETUP Enter the number of model space units <1>:",
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::PaperSpace => vec![CmdOption::new("No", "N"), CmdOption::new("Yes", "Y")],
|
||||
Step::Option => vec![
|
||||
CmdOption::new("Create", "C"),
|
||||
CmdOption::new("Delete objects", "D"),
|
||||
CmdOption::new("Scale viewports", "S"),
|
||||
],
|
||||
Step::Arrangement => vec![
|
||||
CmdOption::new("Single", "S"),
|
||||
CmdOption::new("Engineering", "E"),
|
||||
CmdOption::new("Array", "A"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.step, Step::Delete | Step::Viewports)
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::Delete | Step::Viewports)
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if !handle.is_null() && !self.picked.contains(&handle) {
|
||||
self.picked.push(handle);
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let text = text.trim();
|
||||
let word = text.trim_start_matches('_').to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::PaperSpace => match word.as_str() {
|
||||
"N" | "NO" => self.step = Step::Scale,
|
||||
"" | "Y" | "YES" => {
|
||||
self.step = Step::Option;
|
||||
return Some(CmdResult::MviewSwitchLayout(self.layout.clone()));
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
Step::Scale => {
|
||||
self.scale = positive(text, 1.0)?;
|
||||
self.step = Step::Width;
|
||||
}
|
||||
Step::Width => {
|
||||
self.width = positive(text, f64::NAN).filter(|value| value.is_finite())?;
|
||||
self.step = Step::Height;
|
||||
}
|
||||
Step::Height => {
|
||||
let height = positive(text, f64::NAN).filter(|value| value.is_finite())?;
|
||||
return Some(self.sheet_border(height));
|
||||
}
|
||||
Step::Option => match word.as_str() {
|
||||
"C" | "CREATE" => self.step = Step::Arrangement,
|
||||
"D" | "DELETE" => self.step = Step::Delete,
|
||||
"S" | "SCALE" => self.step = Step::Viewports,
|
||||
_ => return None,
|
||||
},
|
||||
Step::Arrangement => {
|
||||
self.arrangement = match word.as_str() {
|
||||
"" | "S" | "SINGLE" | "1" => Arrangement::Single,
|
||||
"E" | "ENGINEERING" | "2" => Arrangement::Engineering,
|
||||
"A" | "ARRAY" | "3" => Arrangement::Array,
|
||||
_ => return None,
|
||||
};
|
||||
self.step = Step::FirstCorner;
|
||||
}
|
||||
Step::FirstCorner if text.is_empty() => {
|
||||
let ((x0, y0), (x1, y1)) = self.paper_bounds;
|
||||
self.area = (
|
||||
DVec2::new(x0.min(x1), y0.min(y1)),
|
||||
DVec2::new(x0.max(x1), y0.max(y1)),
|
||||
);
|
||||
return Some(self.after_area());
|
||||
}
|
||||
Step::Columns | Step::Rows => {
|
||||
let count = if text.is_empty() {
|
||||
2
|
||||
} else {
|
||||
text.parse::<u32>().ok().filter(|count| (1..=100).contains(count))?
|
||||
};
|
||||
if self.step == Step::Columns {
|
||||
self.columns = count;
|
||||
self.step = Step::Rows;
|
||||
} else {
|
||||
self.rows = count;
|
||||
self.step = Step::GapX;
|
||||
}
|
||||
}
|
||||
Step::GapX | Step::GapY => {
|
||||
let gap = if text.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
crate::entities::common::parse_typed_length(text).filter(|v| *v >= 0.0)?
|
||||
};
|
||||
if self.step == Step::GapY {
|
||||
return Some(self.create(gap));
|
||||
}
|
||||
self.gap_x = gap;
|
||||
self.step = Step::GapY;
|
||||
}
|
||||
Step::PaperUnits => {
|
||||
self.paper_units = positive(text, 1.0)?;
|
||||
self.step = Step::ModelUnits;
|
||||
}
|
||||
Step::ModelUnits => {
|
||||
let model_units = positive(text, 1.0)?;
|
||||
return Some(CmdResult::ScaleViewports {
|
||||
handles: std::mem::take(&mut self.picked),
|
||||
factor: self.paper_units / model_units,
|
||||
});
|
||||
}
|
||||
Step::FirstCorner | Step::SecondCorner | Step::Delete | Step::Viewports => {
|
||||
return None
|
||||
}
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::FirstCorner => {
|
||||
self.first = pt.truncate();
|
||||
self.step = Step::SecondCorner;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::SecondCorner => {
|
||||
let second = pt.truncate();
|
||||
self.area = (self.first.min(second), self.first.max(second));
|
||||
self.after_area()
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Delete => {
|
||||
self.step = Step::Option;
|
||||
let erased = std::mem::take(&mut self.picked);
|
||||
if erased.is_empty() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::ReplaceManyContinue(
|
||||
erased.into_iter().map(|handle| (handle, Vec::new())).collect(),
|
||||
)
|
||||
}
|
||||
Step::Viewports if self.picked.is_empty() => CmdResult::Cancel,
|
||||
Step::Viewports => {
|
||||
self.step = Step::PaperUnits;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Option | Step::Width | Step::Height | Step::SecondCorner => CmdResult::Cancel,
|
||||
_ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MVSETUP"] });
|
||||
Reference in new issue
Block a user