Files
OneCAD-Draft/src/modules/layout/mview.rs
T

582 lines
20 KiB
Rust

// MVIEW — interactive paper-space viewport creation.
use acadrust::entities::{LwPolyline, LwVertex, Viewport};
use acadrust::tables::View;
use acadrust::types::{Vector2, Vector3};
use acadrust::{EntityType, Handle};
use crate::t;
use crate::command::{CadCommand, CmdOption, CmdResult};
use crate::modules::draw::draw::polyline::{
arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::{DVec2, DVec3, Vec2};
// ── Ribbon definition ─────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
ToolDef {
id: "MVIEW",
label: "Viewport",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/viewport.svg")),
event: ModuleEvent::Command("MVIEW".to_string()),
}
}
// ── Command ───────────────────────────────────────────────────────────────
#[derive(Clone, Copy, PartialEq)]
enum Step {
RectangleFirst,
RectangleSecond,
Polygon,
Object,
ChooseView,
DefineNewFirst,
DefineNewSecond,
PlaceView,
}
#[derive(Clone, Copy, PartialEq)]
enum PolygonMode {
Line,
Arc,
}
pub struct MviewCommand {
step: Step,
first: Option<DVec3>,
polygon: Vec<DVec3>,
polygon_bulges: Vec<f64>,
polygon_mode: PolygonMode,
polygon_last_tangent: Option<Vec2>,
view: Option<View>,
views: Vec<View>,
paper_bounds: ((f64, f64), (f64, f64)),
original_layout: String,
}
impl MviewCommand {
pub fn new(
original_layout: String,
paper_bounds: ((f64, f64), (f64, f64)),
views: Vec<View>,
) -> Self {
Self {
step: Step::RectangleFirst,
first: None,
polygon: Vec::new(),
polygon_bulges: Vec::new(),
polygon_mode: PolygonMode::Line,
polygon_last_tangent: None,
view: None,
views,
paper_bounds,
original_layout,
}
}
fn viewport_from_corners(a: DVec3, b: DVec3) -> Option<Viewport> {
let width = (b.x - a.x).abs();
let height = (b.y - a.y).abs();
if width < 1e-6 || height < 1e-6 {
return None;
}
let mut viewport = Viewport::new();
viewport.center = Vector3::new(
(a.x + b.x) / 2.0,
(a.y + b.y) / 2.0,
a.z,
);
viewport.width = width;
viewport.height = height;
viewport.id = 2;
Some(viewport)
}
fn fit_viewport(&self) -> Option<Viewport> {
let ((x0, y0), (x1, y1)) = self.paper_bounds;
Self::viewport_from_corners(
DVec3::new(x0, y0, 0.0),
DVec3::new(x1, y1, 0.0),
)
}
fn placed_viewport(&self, center: DVec3) -> Option<Viewport> {
let view = self.view.as_ref()?;
let source_width = view.width.abs().max(1e-6);
let source_height = view.height.abs().max(1e-6);
let ((x0, y0), (x1, y1)) = self.paper_bounds;
let max_width = ((x1 - x0).abs() * 0.5).max(1e-6);
let max_height = ((y1 - y0).abs() * 0.5).max(1e-6);
let aspect = source_width / source_height;
let (width, height) = if max_width / max_height > aspect {
(max_height * aspect, max_height)
} else {
(max_width, max_width / aspect)
};
let mut viewport = Viewport::new();
viewport.center = Vector3::new(center.x, center.y, center.z);
viewport.width = width;
viewport.height = height;
viewport.id = 2;
viewport.view_target = view.target.clone();
viewport.view_direction = view.direction.clone();
viewport.view_height = source_height;
viewport.custom_scale = height / source_height;
viewport.lens_length = view.lens_length;
viewport.twist_angle = view.twist_angle;
viewport.status.perspective = view.perspective;
Some(viewport)
}
fn polygon_boundary(&self) -> Option<EntityType> {
if self.polygon.len() < 3 {
return None;
}
let mut polyline = LwPolyline::new();
polyline.is_closed = true;
polyline.elevation = self.polygon[0].z;
polyline.vertices = self
.polygon
.iter()
.zip(self.polygon_bulges.iter())
.map(|(point, bulge)| {
let mut vertex = LwVertex::new(Vector2::new(point.x, point.y));
vertex.bulge = *bulge;
vertex
})
.collect();
Some(EntityType::LwPolyline(polyline))
}
fn finish_polygon(&self) -> CmdResult {
match self.polygon_boundary() {
Some(boundary) => CmdResult::MviewCreateClipped {
boundary: Some(boundary),
boundary_handle: Handle::NULL,
},
None => CmdResult::Cancel,
}
}
fn undo_polygon(&mut self) -> CmdResult {
self.polygon.pop();
self.polygon_bulges.pop();
let count = self.polygon.len();
self.polygon_last_tangent = if count >= 2 {
seg_exit_tangent(
self.polygon[count - 2],
self.polygon[count - 1],
self.polygon_bulges[count - 2],
)
} else {
None
};
CmdResult::NeedPoint
}
fn polygon_preview(&self, cursor: DVec3) -> Option<WireModel> {
let last = self.polygon.last()?.as_vec3();
let mut points: Vec<[f32; 3]> = Vec::new();
for index in 0..self.polygon.len().saturating_sub(1) {
let start = self.polygon[index].as_vec3();
let end = self.polygon[index + 1].as_vec3();
let bulge = self.polygon_bulges[index];
if bulge.abs() < 1e-10 {
if points.is_empty() {
points.push([start.x, start.y, start.z]);
}
points.push([end.x, end.y, end.z]);
} else {
let sampled = arc_sample_points(start, bulge, end, 16);
if points.is_empty() {
points.extend_from_slice(&sampled);
} else {
points.extend_from_slice(&sampled[1..]);
}
}
}
let cursor = cursor.as_vec3();
match self.polygon_mode {
PolygonMode::Line => {
if points.is_empty() {
points.push([last.x, last.y, last.z]);
}
points.push([cursor.x, cursor.y, cursor.z]);
}
PolygonMode::Arc => {
let tangent = self
.polygon_last_tangent
.map(|value| value.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(
DVec2::new(last.x as f64, last.y as f64),
tangent,
DVec2::new(cursor.x as f64, cursor.y as f64),
);
let sampled = arc_sample_points(last, bulge, cursor, 16);
if points.is_empty() {
points.extend_from_slice(&sampled);
} else {
points.extend_from_slice(&sampled[1..]);
}
}
}
Some(WireModel::solid(
"mview_preview".to_string(),
points,
WireModel::CYAN,
false,
))
}
fn select_view(&mut self, name: &str) -> Option<CmdResult> {
let view = self
.views
.iter()
.find(|view| view.name.eq_ignore_ascii_case(name.trim()))?
.clone();
self.view = Some(view);
self.step = Step::PlaceView;
Some(CmdResult::NeedPoint)
}
fn preview(points: Vec<DVec3>) -> Option<WireModel> {
if points.len() < 2 {
return None;
}
Some(WireModel::solid_f64(
"mview_preview".to_string(),
points.iter().map(|point| [point.x, point.y, point.z]).collect(),
WireModel::CYAN,
false,
))
}
}
impl CadCommand for MviewCommand {
fn name(&self) -> &'static str {
"MVIEW"
}
fn prompt(&self) -> String {
match self.step {
Step::RectangleFirst => t!(
"MVIEW Specify corner of viewport or [Polygonal/Object/Fit/Insert view]:"
)
.into_owned(),
Step::RectangleSecond => t!("MVIEW Specify opposite corner:").into_owned(),
Step::Polygon if self.polygon.is_empty() => {
t!("MVIEW Polygonal Specify start point:").into_owned()
}
Step::Polygon => {
let mode = match self.polygon_mode {
PolygonMode::Line => t!("Line"),
PolygonMode::Arc => t!("Arc"),
};
t!(
"MVIEW Polygonal [%{mode}] Specify next point or [Arc/Line/Close/Undo] (%{count} points):",
mode = mode,
count = self.polygon.len()
)
.into_owned()
}
Step::Object => {
t!("MVIEW Object Select a circle, full ellipse, or closed polyline:").into_owned()
}
Step::ChooseView if self.views.is_empty() => {
t!("MVIEW Insert view No named views; choose [New]:").into_owned()
}
Step::ChooseView => t!("MVIEW Insert view Choose a named view or [New]:").into_owned(),
Step::DefineNewFirst => {
t!("MVIEW New view Specify first model-space corner:").into_owned()
}
Step::DefineNewSecond => {
t!("MVIEW New view Specify opposite model-space corner:").into_owned()
}
Step::PlaceView => t!("MVIEW Insert view Specify placement point:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::RectangleFirst => vec![
CmdOption::new(t!("Polygonal").as_ref(), "POLYGONAL"),
CmdOption::new(t!("Object").as_ref(), "OBJECT"),
CmdOption::new(t!("Fit").as_ref(), "FIT"),
CmdOption::new(t!("Insert view").as_ref(), "INSERT"),
],
Step::Polygon if !self.polygon.is_empty() => vec![
CmdOption::new(t!("Arc").as_ref(), "ARC"),
CmdOption::new(t!("Line").as_ref(), "LINE"),
CmdOption::new(t!("Close").as_ref(), "CLOSE"),
CmdOption::new(t!("Undo").as_ref(), "UNDO"),
CmdOption::enter(t!("Done").as_ref()),
],
Step::ChooseView => {
let mut options = vec![CmdOption::new(t!("New").as_ref(), "NEW")];
options.extend(
self.views
.iter()
.map(|view| CmdOption::new(&view.name, &view.name)),
);
options
}
_ => Vec::new(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step {
Step::RectangleFirst => {
self.first = Some(pt);
self.step = Step::RectangleSecond;
CmdResult::NeedPoint
}
Step::RectangleSecond => match self
.first
.and_then(|first| Self::viewport_from_corners(first, pt))
{
Some(viewport) => CmdResult::MviewCreate {
viewport,
preserve_view: false,
},
None => CmdResult::NeedPoint,
},
Step::Polygon => {
if let Some(last) = self.polygon.last().copied() {
let last_index = self.polygon.len() - 1;
let bulge = match self.polygon_mode {
PolygonMode::Line => {
let direction = DVec2::new(pt.x - last.x, pt.y - last.y);
if direction.length_squared() > 1e-10 {
self.polygon_last_tangent =
Some(direction.normalize().as_vec2());
}
0.0
}
PolygonMode::Arc => {
let tangent = self
.polygon_last_tangent
.map(|value| value.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(
DVec2::new(last.x, last.y),
tangent,
DVec2::new(pt.x, pt.y),
);
update_tangent_after_arc(
&mut self.polygon_last_tangent,
bulge,
);
bulge
}
};
self.polygon_bulges[last_index] = bulge;
}
if let Some(first) = self.polygon.first() {
let distance_squared =
(pt.x - first.x).powi(2) + (pt.y - first.y).powi(2);
if self.polygon.len() >= 3 && distance_squared < 1e-12 {
return self.finish_polygon();
}
}
self.polygon.push(pt);
self.polygon_bulges.push(0.0);
CmdResult::NeedPoint
}
Step::DefineNewFirst => {
self.first = Some(pt);
self.step = Step::DefineNewSecond;
CmdResult::NeedPoint
}
Step::DefineNewSecond => {
let Some(first) = self.first else {
return CmdResult::NeedPoint;
};
let width = (pt.x - first.x).abs();
let height = (pt.y - first.y).abs();
if width < 1e-6 || height < 1e-6 {
return CmdResult::NeedPoint;
}
let mut view = View::new("");
view.width = width;
view.height = height;
view.target = Vector3::new(
(first.x + pt.x) / 2.0,
(first.y + pt.y) / 2.0,
(first.z + pt.z) / 2.0,
);
self.view = Some(view);
self.step = Step::PlaceView;
CmdResult::MviewSwitchLayout(self.original_layout.clone())
}
Step::PlaceView => match self.placed_viewport(pt) {
Some(viewport) => CmdResult::MviewCreate {
viewport,
preserve_view: true,
},
None => CmdResult::Cancel,
},
Step::Object | Step::ChooseView => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Polygon if self.polygon.len() >= 3 => self.finish_polygon(),
Step::DefineNewFirst | Step::DefineNewSecond => {
CmdResult::MviewCancelToLayout(self.original_layout.clone())
}
_ => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
match self.step {
Step::DefineNewFirst | Step::DefineNewSecond => {
CmdResult::MviewCancelToLayout(self.original_layout.clone())
}
_ => CmdResult::Cancel,
}
}
fn needs_entity_pick(&self) -> bool {
self.step == Step::Object
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
CmdResult::MviewCreateClipped {
boundary: None,
boundary_handle: handle,
}
}
fn wants_text_input(&self) -> bool {
self.step == Step::RectangleFirst
|| self.step == Step::ChooseView
|| (self.step == Step::Polygon && !self.polygon.is_empty())
}
fn wants_text_with_spaces(&self) -> bool {
self.step == Step::ChooseView
}
fn point_step_accepts_keywords(&self) -> bool {
self.step == Step::RectangleFirst
|| (self.step == Step::Polygon && !self.polygon.is_empty())
}
fn window_corner_pick(&self) -> bool {
matches!(self.step, Step::RectangleSecond | Step::DefineNewSecond)
}
fn window_first_corner(&self) -> Option<DVec3> {
self.window_corner_pick().then_some(self.first).flatten()
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim();
let upper = keyword.to_ascii_uppercase();
match self.step {
Step::RectangleFirst => match upper.as_str() {
"P" | "POLYGONAL" => {
self.step = Step::Polygon;
Some(CmdResult::NeedPoint)
}
"O" | "OBJECT" => {
self.step = Step::Object;
Some(CmdResult::NeedPoint)
}
"F" | "FIT" => self.fit_viewport().map(|viewport| CmdResult::MviewCreate {
viewport,
preserve_view: false,
}),
"I" | "INSERT" | "INSERTVIEW" | "INSERT VIEW" => {
self.step = Step::ChooseView;
Some(CmdResult::NeedPoint)
}
_ => None,
},
Step::Polygon => match upper.as_str() {
"A" | "ARC" if !self.polygon.is_empty() => {
self.polygon_mode = PolygonMode::Arc;
Some(CmdResult::NeedPoint)
}
"L" | "LINE" if !self.polygon.is_empty() => {
self.polygon_mode = PolygonMode::Line;
Some(CmdResult::NeedPoint)
}
"C" | "CLOSE" if self.polygon.len() >= 3 => Some(self.finish_polygon()),
"U" | "UNDO" if !self.polygon.is_empty() => {
Some(self.undo_polygon())
}
_ => None,
},
Step::ChooseView => {
if matches!(upper.as_str(), "N" | "NEW") {
self.first = None;
self.step = Step::DefineNewFirst;
Some(CmdResult::MviewSwitchLayout("Model".to_string()))
} else {
self.select_view(keyword)
}
}
_ => None,
}
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
if self.step == Step::Polygon && !self.polygon.is_empty() {
Some(self.undo_polygon())
} else {
None
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step {
Step::RectangleSecond => {
let first = self.first?;
Self::preview(vec![
first,
DVec3::new(pt.x, first.y, first.z),
DVec3::new(pt.x, pt.y, first.z),
DVec3::new(first.x, pt.y, first.z),
first,
])
}
Step::Polygon => self.polygon_preview(pt),
Step::PlaceView => {
let viewport = self.placed_viewport(pt)?;
let half_width = viewport.width / 2.0;
let half_height = viewport.height / 2.0;
Self::preview(vec![
DVec3::new(pt.x - half_width, pt.y - half_height, pt.z),
DVec3::new(pt.x + half_width, pt.y - half_height, pt.z),
DVec3::new(pt.x + half_width, pt.y + half_height, pt.z),
DVec3::new(pt.x - half_width, pt.y + half_height, pt.z),
DVec3::new(pt.x - half_width, pt.y - half_height, pt.z),
])
}
_ => None,
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["MVIEW"] }); // MviewCommand