feat(model): align polysolid workflow

This commit is contained in:
ramox81 committed 2026-09-01 22:01:43 +03:00
1 parent 6d6defa0ad
commit 1dd6b115cf
12 files changed
+857 -250

No files matched your search

Generated
+1 -1
View File
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=48c634995fa60d3928c37bae49b12b421c56c886#48c634995fa60d3928c37bae49b12b421c56c886"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f194a6232066452e3198757f4fe25594fd8b090d#f194a6232066452e3198757f4fe25594fd8b090d"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "48c634995fa60d3928c37bae49b12b421c56c886", features = ["acis", "offset"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f194a6232066452e3198757f4fe25594fd8b090d", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+14 -1
View File
@@ -1636,11 +1636,24 @@ impl OpenCADStudio {
entity,
solid,
history,
erase_source,
} => {
let label = self.history_label_from_active_cmd(i, "SOLID");
let pending = self.begin_undo(i, label, 1, true);
let erase_source = erase_source.filter(|handle| {
self.tabs[i].scene.document.header.delete_objects
&& !self.tabs[i].scene.is_layer_locked(*handle)
});
let pending = self.begin_undo(
i,
label,
1 + usize::from(erase_source.is_some()),
true,
);
let handle = self.add_solid_model(entity, *solid, history);
if !handle.is_null() {
if let Some(source) = erase_source {
self.tabs[i].scene.erase_entities(&[source]);
}
self.tabs[i].dirty = true;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
+18 -22
View File
@@ -1038,32 +1038,28 @@ impl OpenCADStudio {
return Some(self.solid_convtosurface());
}
// POLYSOLID <width> <height> — extrude a selected polyline into a
// wall-like solid.
"POLYSOLID" => {
use crate::command::SelectThenTwoValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenTwoValueCommand::new(
"POLYSOLID",
"POLYSOLID wall width:",
"POLYSOLID wall height:",
has_sel,
);
use crate::modules::model::polysolid_cmd::PolysolidCommand;
let preselected = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.find(|(_, entity)| {
matches!(
entity,
acadrust::EntityType::Line(_)
| acadrust::EntityType::Arc(_)
| acadrust::EntityType::Circle(_)
| acadrust::EntityType::Ellipse(_)
| acadrust::EntityType::LwPolyline(_)
| acadrust::EntityType::Spline(_)
)
})
.map(|(handle, entity)| (handle, entity.clone()));
let c = PolysolidCommand::new(preselected);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("POLYSOLID ") => {
let nums: Vec<f64> = cmd
.split_whitespace()
.skip(1)
.filter_map(|s| s.parse::<f64>().ok())
.collect();
if nums.len() >= 2 && nums[0] > 0.0 && nums[1] > 0.0 {
return Some(self.solid_polysolid(nums[0], nums[1]));
}
self.command_line
.push_info(crate::t!("Usage: POLYSOLID <width> <height> (select a polyline first)").as_ref());
}
// SPLINEFIT — fit a smooth spline through the selected polyline's points.
"SPLINEFIT" | "FITSPLINE" => {
-49
View File
@@ -468,55 +468,6 @@ impl super::OpenCADStudio {
Task::none()
}
/// POLYSOLID — build a wall solid around an exact polyline offset.
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task<Message> {
let i = self.active_tab;
let found: Option<(Handle, EntityType)> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| match e {
EntityType::LwPolyline(_) => Some((*h, (*e).clone())),
_ => None,
});
let Some((handle, entity)) = found else {
self.command_line
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
return Task::none();
};
let Some(result) = crate::scene::model::sweep_model::polysolid(&entity, width, height)
else {
self.command_line
.push_error(crate::t!("POLYSOLID: the kernel could not offset the polyline.").as_ref());
return Task::none();
};
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
self.command_line
.push_error(crate::t!("POLYSOLID: the result could not be encoded as ACIS.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "POLYSOLID");
self.tabs[i].scene.erase_entities(&[handle]);
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&result);
let history = solid_history::brep_op(&result);
let h = self.add_solid_model(EntityType::Solid3D(s3d), result, history);
self.tabs[i].scene.deselect_all();
if !h.is_null() {
self.tabs[i].scene.select_entity(h, false);
}
if !h.is_null() {
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::tf!("POLYSOLID: created a wall solid (width {width}, height {height}).")
.as_ref(),
);
}
Task::none()
}
/// 3DMIRROR — add a mirrored copy of the one selected solid across the plane
/// perpendicular to the X/Y/Z axis (0/1/2) through its centre, keeping the
/// original. A reflection loses handedness, so the kernel reverses every
+1 -98
View File
@@ -1127,104 +1127,6 @@ impl CadCommand for TCountCommand {
}
}
}
/// Generic front-end for a two-value command that operates on the current
/// selection (POLYSOLID width + height on a selected polyline). Gathers a
/// selection first when none is set, prompts for two values, and dispatches
/// `<name> <first> <second>` to the inline handler.
pub struct SelectThenTwoValueCommand {
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
gathering: bool,
selected: Vec<Handle>,
first: Option<String>,
}
impl SelectThenTwoValueCommand {
pub fn new(
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
has_selection: bool,
) -> Self {
Self {
name,
prompt1,
prompt2,
gathering: !has_selection,
selected: Vec::new(),
first: None,
}
}
}
impl CadCommand for SelectThenTwoValueCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
if self.gathering {
crate::t!(
"%{name} select objects, then press Enter:",
name = self.name
)
.into_owned()
} else if self.first.is_none() {
crate::t!(self.prompt1).into_owned()
} else {
crate::t!(self.prompt2).into_owned()
}
}
fn wants_text_input(&self) -> bool {
!self.gathering
}
fn is_selection_gathering(&self) -> bool {
self.gathering
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
self.selected = handles;
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.gathering {
return None;
}
let t = text.trim();
if t.is_empty() {
return None;
}
match &self.first {
None => {
self.first = Some(t.to_string());
// Consumed; `None` would feed the same text back as the second
// value and dispatch `<name> <x> <x>` in one step.
Some(CmdResult::NeedPoint)
}
Some(first) => Some(CmdResult::Dispatch(format!("{} {first} {t}", self.name))),
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.gathering {
if self.selected.is_empty() {
return CmdResult::Cancel;
}
self.gathering = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
}
// ── Result token ──────────────────────────────────────────────────────────
/// Returned by every `CadCommand` method to tell main.rs what to do.
@@ -1278,6 +1180,7 @@ pub enum CmdResult {
entity: EntityType,
solid: Box<cadkernel::brep::Body>,
history: acadrust::objects::SolidHistoryOperation,
erase_source: Option<Handle>,
},
/// Commit an acadrust entity, end the command, and open the in-place text
/// editor on it (used by MLEADER to type the annotation after placement).
+1
View File
@@ -219,6 +219,7 @@ impl CylinderCommand {
entity: EntityType::Solid3D(entity),
solid: Box::new(solid),
history,
erase_source: None,
}
}
+1
View File
@@ -3,6 +3,7 @@
pub mod boolean_cmd;
pub mod cylinder_cmd;
pub mod edge_cmd;
pub mod polysolid_cmd;
pub mod primitive_cmd;
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
+576
View File
@@ -0,0 +1,576 @@
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> {
if self.vertices.len() < 2 {
return None;
}
let local = self
.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 = self.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.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,
))
}
}
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 => format!(
"POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}",
self.height,
self.width,
match self.justification {
PolysolidJustification::Left => "Left",
PolysolidJustification::Center => "Center",
PolysolidJustification::Right => "Right",
},
t!("Specify start point or [Object/Height/Width/Justify] <Object>:")
),
Step::Height => t!("Specify height:").into_owned(),
Step::Width => t!("Specify width:").into_owned(),
Step::Justify => format!(
"Enter justification [Left/Center/Right] <{}>:",
match self.justification {
PolysolidJustification::Left => "Left",
PolysolidJustification::Center => "Center",
PolysolidJustification::Right => "Right",
}
),
Step::Object => t!("Select path 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 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::Justify => {
self.justification = PolysolidJustification::Center;
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)
}
}
+2
View File
@@ -472,6 +472,7 @@ impl PrimitiveCommand {
entity: EntityType::Solid3D(entity),
solid: Box::new(solid),
history,
erase_source: None,
}
}
@@ -1342,6 +1343,7 @@ impl PrimitiveCommand {
entity: EntityType::Solid3D(entity),
solid: Box::new(placed),
history,
erase_source: None,
}
}
None => CmdResult::Cancel,
+139 -1
View File
@@ -2,7 +2,7 @@ use acadrust::entities::Solid3D;
use acadrust::objects::{
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryPyramid, SolidHistorySphere, SolidHistoryTorus,
SolidHistoryPyramid, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
};
use cadkernel::brep::Body;
@@ -21,6 +21,7 @@ pub const GRIP_INNER_RADIUS: usize = 10_006;
pub const GRIP_SIDES: usize = 10_007;
pub const GRIP_MAJOR_RADIUS: usize = 10_008;
pub const GRIP_MINOR_RADIUS: usize = 10_009;
pub const GRIP_POSITION: usize = 10_010;
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
@@ -99,6 +100,7 @@ pub fn has_specialized_primitive_properties(
| SolidHistoryOperation::Sphere(_)
| SolidHistoryOperation::Cone(_)
| SolidHistoryOperation::Cylinder(_)
| SolidHistoryOperation::Sweep(_)
)
)
}
@@ -114,10 +116,77 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
}
SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Sweep(value) => world_point(
value.base.transform,
[
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
],
),
_ => None,
}
}
fn sweep_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistorySweep,
) -> Vec<PropSection> {
let Some(position) = reference_point(&SolidHistoryOperation::Sweep(value.clone())) else {
return Vec::new();
};
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, _) =
displayed_history_state(object_show_history, show_history_mode);
let show_value = if show_history { "Yes" } else { "No" };
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Sweep").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
],
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::ReadOnly(
if record_history { "Record" } else { "None" }.to_string(),
),
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: PropValue::ReadOnly(show_value.to_string()),
},
],
},
]
}
fn cone_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -558,6 +627,9 @@ pub fn primitive_properties(
SolidHistoryOperation::Cylinder(value) => {
return cylinder_properties(document, handle, value)
}
SolidHistoryOperation::Sweep(value) => {
return sweep_properties(document, handle, value)
}
SolidHistoryOperation::Torus(value) => vec![
history_prop(
t!("Outer Radius").as_ref(),
@@ -812,6 +884,40 @@ fn apply_cylinder_position_property(
Some(true)
}
fn apply_sweep_position_property(
value: &mut SolidHistorySweep,
field: &str,
text: &str,
) -> Option<bool> {
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
PROP_POSITION_Z => 2,
_ => return None,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let reference = current.transform_point3(glam::DVec3::new(
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
));
if !reference.is_finite() || (target - reference[axis]).abs() <= 1e-12 {
return Some(false);
}
let mut delta = glam::DVec3::ZERO;
delta[axis] = target - reference[axis];
let updated = glam::DMat4::from_translation(delta) * current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
Some(true)
}
fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
if value.minor_radius <= value.major_radius {
value.x_radius = value.major_radius;
@@ -856,6 +962,11 @@ pub fn apply_primitive_property(
return applied;
}
}
if let SolidHistoryOperation::Sweep(sweep_value) = operation {
if let Some(applied) = apply_sweep_position_property(sweep_value, field, value) {
return applied;
}
}
let Some(number) = (if field == PROP_SIDES {
value.trim().parse::<f64>().ok()
} else {
@@ -1334,6 +1445,17 @@ pub fn primitive_grips(
None,
);
}
SolidHistoryOperation::Sweep(value) => add(
GRIP_POSITION,
value.base.transform,
[
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
],
GripShape::Square,
None,
),
_ => {}
}
grips
@@ -1488,6 +1610,22 @@ pub fn apply_primitive_grip(
}
_ => return false,
},
SolidHistoryOperation::Sweep(value) if grip_id == GRIP_POSITION => {
let Some(current) = matrix(value.base.transform) else {
return false;
};
let reference = current.transform_point3(glam::DVec3::new(
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
));
let delta = world - reference;
if !delta.is_finite() || delta.length_squared() <= 1e-24 {
return false;
}
value.base.transform =
(glam::DMat4::from_translation(delta) * current).to_cols_array();
}
_ => return false,
}
true
+103 -77
View File
@@ -1,8 +1,13 @@
// Exact profile sweeps stored as kernel B-reps and ACIS.
use cadkernel::brep::{self, Body};
use cadkernel::geom2d::{offset_polyline, Arc, Curve, EllipseArc, Line, Polyline};
use cadkernel::geom2d::{Arc, Curve, EllipseArc, Line};
use cadkernel::space::{PlanarCurve, Plane, Vec3};
use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex};
use acadrust::objects::{
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep,
};
use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType;
use crate::entities::curve::entity_curve;
@@ -162,88 +167,109 @@ pub fn lofted(profiles: &[EntityType]) -> Option<Body> {
brep::loft(&sections)
}
/// Builds an exact wall solid around a polyline centreline.
pub fn polysolid(entity: &EntityType, width: f64, height: f64) -> Option<Body> {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PolysolidJustification {
Left,
Center,
Right,
}
fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
EntityType::Line(value) => Some(EmbeddedEntity::Line(value.clone())),
EntityType::Arc(value) => Some(EmbeddedEntity::Arc(value.clone())),
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
EntityType::Spline(_) => {
let planar = entity_curve(entity)?;
let samples = planar.curve.tessellate_within(1e-4);
if samples.len() < 2 {
return None;
}
let mut path = LwPolyline::new();
path.vertices = samples
.into_iter()
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
.collect();
path.is_closed = planar.curve.is_closed();
let plane = crate::command::WorkingPlane::new(
glam::DVec3::from_array(planar.plane.origin),
glam::DVec3::from_array(planar.plane.x_axis),
glam::DVec3::from_array(planar.plane.y_axis),
);
let EntityType::LwPolyline(path) = plane.place_entity(EntityType::LwPolyline(path))
else {
return None;
};
Some(EmbeddedEntity::LwPolyline(path))
}
_ => None,
}
}
/// Builds a history-backed wall solid along a supported planar curve.
pub fn polysolid(
entity: &EntityType,
width: f64,
height: f64,
justification: PolysolidJustification,
) -> Option<(Body, SolidHistoryOperation)> {
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height.abs() <= 1e-12 {
return None;
}
let planar = entity_curve(entity)?;
let Curve::Polyline(source) = planar.curve else {
let start_local = planar.curve.point_at(0.0);
let tangent_local = Vec3::from([
planar.curve.tangent_at(0.0)[0],
planar.curve.tangent_at(0.0)[1],
0.0,
])
.normalize()?;
let start = Vec3::from(planar.plane.point_at(start_local));
let tangent = (Vec3::from(planar.plane.x_axis) * tangent_local.x
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
.normalize()?;
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
let side = tangent.cross(normal).normalize()?;
let offset = match justification {
PolysolidJustification::Left => 0.0,
PolysolidJustification::Center => -width * 0.5,
PolysolidJustification::Right => -width,
};
let profile_origin = start + side * offset;
let profile_plane = crate::command::WorkingPlane::new(
glam::DVec3::from_array(profile_origin.to_array()),
glam::DVec3::from_array(side.to_array()),
glam::DVec3::from_array(normal.to_array()),
);
let mut profile = LwPolyline::new();
profile.vertices = [[0.0, 0.0], [width, 0.0], [width, height], [0.0, height]]
.into_iter()
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
.collect();
profile.is_closed = true;
let EntityType::LwPolyline(profile) =
profile_plane.place_entity(EntityType::LwPolyline(profile))
else {
return None;
};
let (left_pick, right_pick) = offset_picks(&source, width)?;
let mut left = offset_polyline(&source, width * 0.5, left_pick);
let mut right = offset_polyline(&source, width * 0.5, right_pick);
if left.len() != 1 || right.len() != 1 {
return None;
}
let left = left.remove(0);
let right = right.remove(0);
let normal = Vec3::from(planar.plane.normal()?);
let direction = (normal * height).to_array();
if source.closed {
if !left.closed || !right.closed {
return None;
}
let mut profiles = [left, right]
.into_iter()
.map(|polyline| {
let area = Curve::Polyline(polyline.clone()).enclosed_area().abs();
(area, Curve::Polyline(polyline).segments())
})
.collect::<Vec<_>>();
profiles.sort_by(|first, second| second.0.total_cmp(&first.0));
if profiles[0].0 <= profiles[1].0 || profiles[1].0 <= 1e-12 {
return None;
}
return brep::extrude_region(
planar.plane,
&[profiles.remove(0).1, profiles.remove(0).1],
direction,
);
}
if left.closed || right.closed {
return None;
}
let left_start = left.vertices.first()?.position;
let left_end = left.vertices.last()?.position;
let right_start = right.vertices.first()?.position;
let right_end = right.vertices.last()?.position;
let mut outline = Curve::Polyline(left).segments();
outline.push(Curve::Line(Line {
start: left_end,
end: right_end,
}));
let mut back = Curve::Polyline(right).segments();
back.reverse();
outline.extend(back);
outline.push(Curve::Line(Line {
start: right_start,
end: left_start,
}));
brep::extrude(planar.plane, &outline, direction)
}
fn offset_picks(polyline: &Polyline, width: f64) -> Option<([f64; 2], [f64; 2])> {
let start = polyline.vertices.first()?.position;
let next = polyline.vertices.get(1)?.position;
let along = if let Some(arc) = polyline.segment_arc(0) {
let near = arc.sample(1e-6);
[near[0] - start[0], near[1] - start[1]]
} else {
[next[0] - start[0], next[1] - start[1]]
};
let length = along[0].hypot(along[1]);
if length <= 1e-12 {
return None;
}
let side = [-along[1] / length * width, along[0] / length * width];
Some((
[start[0] + side[0], start[1] + side[1]],
[start[0] - side[0], start[1] - side[1]],
))
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
let operation = SolidHistoryOperation::Sweep(SolidHistorySweep {
base,
operation_major: 1,
direction: Vector3::new(normal.x * height, normal.y * height, normal.z * height),
sweep_entity: Some(EmbeddedEntity::LwPolyline(profile)),
path_entity: Some(embedded_path(entity)?),
scale_factor: 1.0,
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(start.x, start.y, start.z),
..SolidHistorySweep::default()
});
let body = cadkernel::acis::rebuild_body(&operation).ok()?;
Some((body, operation))
}
fn circular_loft(profiles: &[EntityType]) -> Option<Body> {