forked from 1b-agent/opencadstudio-upstream
380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
// 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"]
|
|
});
|