Files
OneCAD-Draft/src/modules/model/polysolid_cmd.rs
T

646 lines
22 KiB
Rust

use std::sync::{Mutex, OnceLock};
use acadrust::entities::{LwPolyline, LwVertex, Solid3D};
use acadrust::types::Vector2;
use acadrust::{EntityType, Handle};
use glam::{DVec2, DVec3};
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::modules::draw::draw::polyline::{
arc_sample_points, compute_bulge, seg_exit_tangent,
};
use crate::scene::model::solid_model;
use crate::scene::model::sweep_model::{self, PolysolidJustification};
use crate::scene::model::wire_model::WireModel;
use crate::t;
#[derive(Clone, Copy)]
struct Defaults {
height: f64,
width: f64,
justification: PolysolidJustification,
}
fn defaults() -> &'static Mutex<Defaults> {
static DEFAULTS: OnceLock<Mutex<Defaults>> = OnceLock::new();
DEFAULTS.get_or_init(|| {
Mutex::new(Defaults {
height: 80.0,
width: 5.0,
justification: PolysolidJustification::Center,
})
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Step {
Start,
Height,
Width,
Justify,
Object,
Line,
Arc,
ArcDirection,
ArcSecond,
ArcEnd,
}
pub struct PolysolidCommand {
step: Step,
return_step: Step,
plane: WorkingPlane,
vertices: Vec<DVec3>,
bulges: Vec<f64>,
height: f64,
width: f64,
justification: PolysolidJustification,
pending_direction: Option<DVec2>,
pending_second: Option<DVec3>,
picked: Option<EntityType>,
preselected: Option<(Handle, EntityType)>,
}
impl PolysolidCommand {
pub fn new(preselected: Option<(Handle, EntityType)>) -> Self {
let saved = defaults().lock().map(|value| *value).unwrap_or(Defaults {
height: 80.0,
width: 5.0,
justification: PolysolidJustification::Center,
});
Self {
step: Step::Start,
return_step: Step::Start,
plane: WorkingPlane::default(),
vertices: Vec::new(),
bulges: Vec::new(),
height: saved.height,
width: saved.width,
justification: saved.justification,
pending_direction: None,
pending_second: None,
picked: None,
preselected,
}
}
fn remember(&self) {
if let Ok(mut saved) = defaults().lock() {
*saved = Defaults {
height: self.height,
width: self.width,
justification: self.justification,
};
}
}
fn supported(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Line(_)
| EntityType::Arc(_)
| EntityType::Circle(_)
| EntityType::Ellipse(_)
| EntityType::LwPolyline(_)
| EntityType::Spline(_)
)
}
fn path_entity(&self, closed: bool) -> Option<EntityType> {
self.path_entity_from(&self.vertices, &self.bulges, closed)
}
fn path_entity_from(
&self,
vertices: &[DVec3],
bulges: &[f64],
closed: bool,
) -> Option<EntityType> {
if vertices.len() < 2 {
return None;
}
let local = vertices
.iter()
.map(|point| self.plane.to_local(*point))
.collect::<Vec<_>>();
let mut path = LwPolyline::new();
path.vertices = local
.iter()
.enumerate()
.map(|(index, point)| {
let mut vertex = LwVertex::new(Vector2::new(point.x, point.y));
vertex.bulge = bulges.get(index).copied().unwrap_or(0.0);
vertex
})
.collect();
path.elevation = local[0].z;
path.is_closed = closed;
Some(self.plane.place_entity(EntityType::LwPolyline(path)))
}
fn commit(&self, path: EntityType, erase_source: Option<Handle>) -> CmdResult {
let Some((solid, history)) = sweep_model::polysolid(
&path,
self.width,
self.height,
self.justification,
) else {
return CmdResult::NeedPoint;
};
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid) else {
return CmdResult::Cancel;
};
let mut entity = Solid3D::new();
entity.set_sat_document(&document);
entity.wires = solid_model::edge_wires(&solid);
CmdResult::CommitSolid {
entity: EntityType::Solid3D(entity),
solid: Box::new(solid),
history,
erase_source,
}
}
fn finish(&self, closed: bool) -> CmdResult {
self.path_entity(closed)
.map(|path| self.commit(path, None))
.unwrap_or(CmdResult::Cancel)
}
fn close(&mut self) -> CmdResult {
if self.vertices.len() < 3 {
return CmdResult::NeedPoint;
}
if self.step == Step::Arc {
let last = *self.vertices.last().unwrap();
let first = self.vertices[0];
let bulge = compute_bulge(
self.plane.to_local(last).truncate(),
self.pending_direction.unwrap_or_else(|| self.last_tangent()),
self.plane.to_local(first).truncate(),
);
if let Some(value) = self.bulges.last_mut() {
*value = bulge;
}
}
self.finish(true)
}
fn undo(&mut self) -> CmdResult {
if self.vertices.pop().is_some() {
self.bulges.pop();
if let Some(last) = self.bulges.last_mut() {
*last = 0.0;
}
}
self.pending_direction = None;
self.pending_second = None;
if self.vertices.is_empty() {
self.step = Step::Start;
} else if matches!(self.step, Step::Arc | Step::ArcDirection | Step::ArcSecond | Step::ArcEnd)
{
self.step = Step::Arc;
} else {
self.step = Step::Line;
}
CmdResult::NeedPoint
}
fn last_tangent(&self) -> DVec2 {
let count = self.vertices.len();
if count < 2 {
return DVec2::X;
}
let a = self.plane.to_local(self.vertices[count - 2]);
let b = self.plane.to_local(self.vertices[count - 1]);
seg_exit_tangent(a, b, self.bulges[count - 2])
.map(|value| value.as_dvec2())
.unwrap_or(DVec2::X)
}
fn add_segment(&mut self, point: DVec3, bulge: f64) -> CmdResult {
if let Some(last) = self.bulges.last_mut() {
*last = bulge;
}
self.vertices.push(point);
self.bulges.push(0.0);
self.pending_direction = None;
self.pending_second = None;
CmdResult::NeedPoint
}
fn three_point_bulge(&self, start: DVec3, middle: DVec3, end: DVec3) -> f64 {
let a3 = self.plane.to_local(start);
let m3 = self.plane.to_local(middle);
let b3 = self.plane.to_local(end);
let a = DVec2::new(a3.x, a3.y);
let m = DVec2::new(m3.x, m3.y);
let b = DVec2::new(b3.x, b3.y);
let determinant = 2.0
* (a.x * (m.y - b.y) + m.x * (b.y - a.y) + b.x * (a.y - m.y));
if determinant.abs() <= 1e-12 {
return 0.0;
}
let aa = a.length_squared();
let mm = m.length_squared();
let bb = b.length_squared();
let center = DVec2::new(
(aa * (m.y - b.y) + mm * (b.y - a.y) + bb * (a.y - m.y))
/ determinant,
(aa * (b.x - m.x) + mm * (a.x - b.x) + bb * (m.x - a.x))
/ determinant,
);
let start_angle = (a - center).y.atan2((a - center).x);
let middle_angle = (m - center).y.atan2((m - center).x);
let end_angle = (b - center).y.atan2((b - center).x);
let ccw = (end_angle - start_angle).rem_euclid(std::f64::consts::TAU);
let middle_ccw =
(middle_angle - start_angle).rem_euclid(std::f64::consts::TAU);
let sweep = if middle_ccw <= ccw + 1e-12 {
ccw
} else {
ccw - std::f64::consts::TAU
};
(sweep / 4.0).tan()
}
fn value_step(&mut self, step: Step) {
self.return_step = if self.vertices.is_empty() {
Step::Start
} else {
self.step
};
self.step = step;
}
fn path_preview(&self, cursor: DVec3) -> Option<WireModel> {
let start = *self.vertices.last()?;
let start_local = self.plane.to_local(start);
let cursor_local = self.plane.to_local(cursor);
let points = match self.step {
Step::Arc => {
let bulge = compute_bulge(
start_local.truncate(),
self.pending_direction.unwrap_or_else(|| self.last_tangent()),
cursor_local.truncate(),
);
arc_sample_points(start_local.as_vec3(), bulge, cursor_local.as_vec3(), 24)
.into_iter()
.map(|point| self.plane.to_world(DVec3::new(
point[0] as f64,
point[1] as f64,
point[2] as f64,
)).to_array())
.collect()
}
Step::ArcEnd => {
let middle = self.pending_second?;
let bulge = self.three_point_bulge(start, middle, cursor);
arc_sample_points(start_local.as_vec3(), bulge, cursor_local.as_vec3(), 24)
.into_iter()
.map(|point| self.plane.to_world(DVec3::new(
point[0] as f64,
point[1] as f64,
point[2] as f64,
)).to_array())
.collect()
}
Step::ArcDirection => vec![start.to_array(), cursor.to_array()],
_ => vec![start.to_array(), cursor.to_array()],
};
Some(WireModel::solid_f64(
"POLYSOLID_PREVIEW".to_string(),
points,
WireModel::CYAN,
false,
))
}
fn solid_preview(&self, cursor: DVec3) -> Option<Vec<WireModel>> {
if !matches!(self.step, Step::Line | Step::Arc | Step::ArcEnd) {
return None;
}
let start = *self.vertices.last()?;
if (cursor - start).length_squared() <= 1e-12 {
return None;
}
let mut vertices = self.vertices.clone();
let mut bulges = self.bulges.clone();
let segment_bulge = match self.step {
Step::Arc => compute_bulge(
self.plane.to_local(start).truncate(),
self.pending_direction.unwrap_or_else(|| self.last_tangent()),
self.plane.to_local(cursor).truncate(),
),
Step::ArcEnd => {
self.three_point_bulge(start, self.pending_second?, cursor)
}
_ => 0.0,
};
if let Some(last) = bulges.last_mut() {
*last = segment_bulge;
}
vertices.push(cursor);
bulges.push(0.0);
let path = self.path_entity_from(&vertices, &bulges, false)?;
let (solid, _) = sweep_model::polysolid(
&path,
self.width,
self.height,
self.justification,
)?;
let previews = solid_model::edge_wires(&solid)
.into_iter()
.enumerate()
.filter_map(|(index, wire)| {
(wire.points.len() >= 2).then(|| {
WireModel::solid_f64(
format!("POLYSOLID_PREVIEW_{index}"),
wire.points
.into_iter()
.map(|point| [point.x, point.y, point.z])
.collect(),
WireModel::CYAN,
false,
)
})
})
.collect::<Vec<_>>();
(!previews.is_empty()).then_some(previews)
}
}
impl CadCommand for PolysolidCommand {
fn name(&self) -> &'static str {
"POLYSOLID"
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn prompt(&self) -> String {
match self.step {
Step::Start => crate::tf!(
"POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}",
self.height,
self.width,
match self.justification {
PolysolidJustification::Left => crate::t!("Left"),
PolysolidJustification::Center => crate::t!("Center"),
PolysolidJustification::Right => crate::t!("Right"),
},
t!("Specify start point or [Object/Height/Width/Justify] <Object>:")
).into_owned(),
Step::Height => crate::tf!("Specify height <{:.4}>:", self.height).into_owned(),
Step::Width => crate::tf!("Specify width <{:.4}>:", self.width).into_owned(),
Step::Justify => crate::tf!(
"Enter justification [Left/Center/Right] <{}>:",
match self.justification {
PolysolidJustification::Left => crate::t!("Left"),
PolysolidJustification::Center => crate::t!("Center"),
PolysolidJustification::Right => crate::t!("Right"),
}
).into_owned(),
Step::Object => t!("Select object:").into_owned(),
Step::Line if self.vertices.len() >= 3 => {
t!("Specify next point or [Arc/Close/Undo]:").into_owned()
}
Step::Line => t!("Specify next point or [Arc/Undo]:").into_owned(),
Step::Arc if self.vertices.len() >= 3 => {
t!("Specify endpoint of arc or [Close/Direction/Line/Second point/Undo]:")
.into_owned()
}
Step::Arc => {
t!("Specify endpoint of arc or [Direction/Line/Second point/Undo]:").into_owned()
}
Step::ArcDirection => {
t!("Specify the tangent direction for the start point of arc:").into_owned()
}
Step::ArcSecond => t!("Specify second point on arc:").into_owned(),
Step::ArcEnd => t!("Specify end point of arc:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Start => vec![
CmdOption::new(t!("Object").as_ref(), "O"),
CmdOption::new(t!("Height").as_ref(), "H"),
CmdOption::new(t!("Width").as_ref(), "W"),
CmdOption::new(t!("Justify").as_ref(), "J"),
],
Step::Line => {
let mut options = vec![CmdOption::new(t!("Arc").as_ref(), "A")];
if self.vertices.len() >= 3 {
options.push(CmdOption::new(t!("Close").as_ref(), "C"));
}
options.push(CmdOption::new(t!("Undo").as_ref(), "U"));
options
}
Step::Arc => {
let mut options = Vec::new();
if self.vertices.len() >= 3 {
options.push(CmdOption::new(t!("Close").as_ref(), "C"));
}
options.extend([
CmdOption::new(t!("Direction").as_ref(), "D"),
CmdOption::new(t!("Line").as_ref(), "L"),
CmdOption::new(t!("Second point").as_ref(), "S"),
CmdOption::new(t!("Undo").as_ref(), "U"),
]);
options
}
_ => Vec::new(),
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Start => {
self.vertices.push(point);
self.bulges.push(0.0);
self.step = Step::Line;
CmdResult::NeedPoint
}
Step::Line => self.add_segment(point, 0.0),
Step::Arc => {
let start = *self.vertices.last().unwrap_or(&point);
let a = self.plane.to_local(start).truncate();
let b = self.plane.to_local(point).truncate();
let tangent = self.pending_direction.unwrap_or_else(|| self.last_tangent());
let bulge = compute_bulge(a, tangent, b);
self.add_segment(point, bulge)
}
Step::ArcDirection => {
let start = *self.vertices.last().unwrap_or(&point);
let direction = self.plane.to_local(point) - self.plane.to_local(start);
if direction.truncate().length_squared() > 1e-12 {
self.pending_direction = Some(direction.truncate().normalize());
self.step = Step::Arc;
}
CmdResult::NeedPoint
}
Step::ArcSecond => {
self.pending_second = Some(point);
self.step = Step::ArcEnd;
CmdResult::NeedPoint
}
Step::ArcEnd => {
let start = *self.vertices.last().unwrap_or(&point);
let middle = self.pending_second.unwrap_or(point);
let bulge = self.three_point_bulge(start, middle, point);
self.step = Step::Arc;
self.add_segment(point, bulge)
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Start => {
if let Some((handle, entity)) = self.preselected.take() {
return self.commit(entity, Some(handle));
}
self.step = Step::Object;
CmdResult::NeedPoint
}
Step::Line | Step::Arc if self.vertices.len() >= 2 => self.finish(false),
Step::Height | Step::Width | Step::Justify => {
self.remember();
self.step = self.return_step;
CmdResult::NeedPoint
}
_ => CmdResult::Cancel,
}
}
fn wants_text_input(&self) -> bool {
!matches!(self.step, Step::Object | Step::ArcDirection | Step::ArcSecond | Step::ArcEnd)
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::Start | Step::Line | Step::Arc)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let value = text.trim();
let keyword = value.to_uppercase();
match self.step {
Step::Height => {
let number = crate::entities::common::parse_length(value)?;
if number > 0.0 && number.is_finite() {
self.height = number;
self.remember();
self.step = self.return_step;
}
Some(CmdResult::NeedPoint)
}
Step::Width => {
let number = crate::entities::common::parse_length(value)?;
if number > 0.0 && number.is_finite() {
self.width = number;
self.remember();
self.step = self.return_step;
}
Some(CmdResult::NeedPoint)
}
Step::Justify => {
self.justification = match keyword.as_str() {
"L" | "LEFT" => PolysolidJustification::Left,
"C" | "CENTER" => PolysolidJustification::Center,
"R" | "RIGHT" => PolysolidJustification::Right,
_ => return None,
};
self.remember();
self.step = self.return_step;
Some(CmdResult::NeedPoint)
}
Step::Start => match keyword.as_str() {
"O" | "OBJECT" => {
self.step = Step::Object;
Some(CmdResult::NeedPoint)
}
"H" | "HEIGHT" => {
self.value_step(Step::Height);
Some(CmdResult::NeedPoint)
}
"W" | "WIDTH" => {
self.value_step(Step::Width);
Some(CmdResult::NeedPoint)
}
"J" | "JUSTIFY" => {
self.value_step(Step::Justify);
Some(CmdResult::NeedPoint)
}
_ => None,
},
Step::Line => match keyword.as_str() {
"A" | "ARC" => {
self.step = Step::Arc;
Some(CmdResult::NeedPoint)
}
"C" | "CLOSE" if self.vertices.len() >= 3 => Some(self.close()),
"U" | "UNDO" => Some(self.undo()),
_ => None,
},
Step::Arc => match keyword.as_str() {
"D" | "DIRECTION" => {
self.step = Step::ArcDirection;
Some(CmdResult::NeedPoint)
}
"L" | "LINE" => {
self.step = Step::Line;
Some(CmdResult::NeedPoint)
}
"S" | "SECOND" | "SECOND POINT" => {
self.step = Step::ArcSecond;
Some(CmdResult::NeedPoint)
}
"C" | "CLOSE" if self.vertices.len() >= 3 => Some(self.close()),
"U" | "UNDO" => Some(self.undo()),
_ => None,
},
_ => None,
}
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
(!self.vertices.is_empty()).then(|| self.undo())
}
fn needs_entity_pick(&self) -> bool {
self.step == Step::Object
}
fn inject_before_entity_pick(&self) -> bool {
self.step == Step::Object
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
let Some(entity) = self.picked.take() else {
return CmdResult::NeedPoint;
};
if !Self::supported(&entity) {
return CmdResult::NeedPoint;
}
self.commit(entity, Some(handle))
}
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
self.path_preview(point)
}
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
self.solid_preview(point)
.unwrap_or_else(|| self.path_preview(point).into_iter().collect())
}
}