diff --git a/Cargo.lock b/Cargo.lock index 73374e491..d7220eec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1ad942cc34b592917ca5fff9936afb902109a29d#1ad942cc34b592917ca5fff9936afb902109a29d" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7d7f8dabe64cbacdeea7063d3ebde5d170f9a365#7d7f8dabe64cbacdeea7063d3ebde5d170f9a365" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index c7b275d3e..e32fb16e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "1ad942cc34b592917ca5fff9936afb902109a29d", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7d7f8dabe64cbacdeea7063d3ebde5d170f9a365", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index bf67a0ef6..232b037b8 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3476,13 +3476,12 @@ impl OpenCADStudio { }; use crate::scene::model::sweep_model; let path = match extent { - ExtrudeExtent::Path(handle) => { - if delete_sources && self.reject_locked_edit(i, handle) { - self.tabs[i].active_cmd = None; - return Task::none(); - } - self.tabs[i].scene.document.get_entity(handle).cloned().map(|entity| (handle, entity)) - } + ExtrudeExtent::Path(handle) => self.tabs[i] + .scene + .document + .get_entity(handle) + .cloned() + .map(|entity| (handle, entity)), _ => None, }; if matches!(extent, ExtrudeExtent::Path(_)) && path.is_none() { @@ -3515,22 +3514,16 @@ impl OpenCADStudio { ExtrudeExtent::Direction(direction) => Some(direction), ExtrudeExtent::Path(_) => None, }; - let path_direction = path.as_ref().and_then(|(_, path)| { - crate::entities::curve::entity_curve(path).and_then(|curve| { - let cadkernel::geom2d::Curve::Line(line) = curve.curve else { - return None; - }; - let start = glam::DVec3::from_array(curve.plane.point_at(line.start)); - let end = glam::DVec3::from_array(curve.plane.point_at(line.end)); - Some(end - start) - }) - }); + let path_direction = path + .as_ref() + .and_then(|(_, path)| sweep_model::straight_path_direction(path)) + .map(glam::DVec3::from_array); // The creation mode controls closed profiles. Open curves // always create sheet surfaces. let creates_surface = matches!(mode, ExtrudeMode::Surface) || !closed; let body = match (creates_surface, &path, direction) { - (false, Some((_, path)), _) if taper_angle.abs() <= 1e-12 => { - sweep_model::extruded_along_path(&entity, path) + (false, Some((_, path)), _) => { + sweep_model::extruded_along_path(&entity, path, taper_angle) } (false, None, Some(direction)) => { sweep_model::extruded_direction( @@ -3547,7 +3540,11 @@ impl OpenCADStudio { ) } (true, Some(_), _) => path_direction.and_then(|direction| { - sweep_model::extruded_surface(&entity, direction.to_array(), 0.0) + sweep_model::extruded_surface( + &entity, + direction.to_array(), + taper_angle, + ) }), _ => None, }; @@ -3565,13 +3562,25 @@ impl OpenCADStudio { ) } else { let direction = direction.unwrap_or(glam::DVec3::ZERO); - let history = sweep_model::extrusion_history( - &entity, - path.as_ref().map(|(_, entity)| entity), - direction.to_array(), - taper_angle, - profile.plane.origin, - ) + let history = path + .as_ref() + .and_then(|(_, path)| { + sweep_model::sweep_history( + &entity, + path, + taper_angle, + profile.plane.origin, + ) + }) + .or_else(|| { + sweep_model::extrusion_history( + &entity, + None, + direction.to_array(), + taper_angle, + profile.plane.origin, + ) + }) .unwrap_or_else(|| crate::scene::model::solid_history::brep_op(&body)); self.add_solid_model(empty_solid3d(), body, history) }; @@ -3585,9 +3594,6 @@ impl OpenCADStudio { } } if delete_sources && !created_handles.is_empty() { - if let Some((path_handle, _)) = path { - consumed.push(path_handle); - } consumed.sort_unstable_by_key(|handle| handle.value()); consumed.dedup(); self.tabs[i].scene.erase_entities(&consumed); diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index d3720e3c8..dca2a5c4e 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -1,5 +1,7 @@ // Profile-based kernel solid commands stored as exact ACIS data. +use std::sync::{Mutex, OnceLock}; + use acadrust::{entities::Solid3D, EntityType}; use glam::DVec3; @@ -17,9 +19,29 @@ pub struct ExtrudeCommand { direction_start: Option, mode: ExtrudeMode, taper_angle: f64, + last_height: Option, + taper_return: ExtrudeStep, color: [f32; 4], } +#[derive(Clone, Copy)] +struct ExtrudeDefaults { + mode: ExtrudeMode, + height: Option, + taper_angle: f64, +} + +fn extrude_defaults() -> &'static Mutex { + static DEFAULTS: OnceLock> = OnceLock::new(); + DEFAULTS.get_or_init(|| { + Mutex::new(ExtrudeDefaults { + mode: ExtrudeMode::Solid, + height: None, + taper_angle: 0.0, + }) + }) +} + #[derive(Clone, Copy, PartialEq, Eq)] enum ExtrudeStep { Pick, @@ -30,10 +52,12 @@ enum ExtrudeStep { Path, Taper, Expression, + TaperExpression, } impl ExtrudeCommand { pub fn new_named(name: &str, color: [f32; 4]) -> Self { + let defaults = *extrude_defaults().lock().unwrap_or_else(|error| error.into_inner()); Self { command_name: match name { "THICKEN" => "THICKEN", @@ -44,8 +68,10 @@ impl ExtrudeCommand { anchor: DVec3::ZERO, profile_direction: None, direction_start: None, - mode: ExtrudeMode::Solid, - taper_angle: 0.0, + mode: defaults.mode, + taper_angle: defaults.taper_angle, + last_height: defaults.height, + taper_return: ExtrudeStep::Height, color, } } @@ -65,6 +91,16 @@ impl ExtrudeCommand { } fn finish(&self, extent: ExtrudeExtent) -> CmdResult { + if let Some(height) = match extent { + ExtrudeExtent::Height(height) => Some(height), + ExtrudeExtent::Direction(direction) => Some(direction.length()), + ExtrudeExtent::Path(_) => None, + } { + extrude_defaults() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .height = Some(height); + } CmdResult::ExtrudeEntities { handles: self.handles.clone(), extent, @@ -82,13 +118,28 @@ impl CadCommand for ExtrudeCommand { fn prompt(&self) -> String { match self.step { ExtrudeStep::Pick => t!("EXTRUDE Select objects to extrude or [Mode] (Enter to finish):").into_owned(), - ExtrudeStep::Mode => t!("EXTRUDE Creation mode [Solid/Surface]:").into_owned(), - ExtrudeStep::Height => t!("EXTRUDE Specify height or [Direction/Path/Taper angle/Expression]:").into_owned(), + ExtrudeStep::Mode => format!( + "{} <{}>:", + t!("EXTRUDE Creation mode [Solid/Surface]"), + if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" } + ), + ExtrudeStep::Height => { + let base = t!("EXTRUDE Specify height or [Direction/Path/Taper angle/Expression]"); + self.last_height.map_or_else( + || format!("{base}:"), + |height| format!("{base} <{}>:", crate::entities::common::format_length(height)), + ) + } ExtrudeStep::DirectionStart => t!("EXTRUDE Start point of direction:").into_owned(), ExtrudeStep::DirectionEnd => t!("EXTRUDE End point of direction:").into_owned(), - ExtrudeStep::Path => t!("EXTRUDE Select extrusion path:").into_owned(), - ExtrudeStep::Taper => t!("EXTRUDE Specify taper angle:").into_owned(), + ExtrudeStep::Path => t!("EXTRUDE Select extrusion path or [Taper angle]:").into_owned(), + ExtrudeStep::Taper => format!( + "{} <{}>:", + t!("EXTRUDE Specify taper angle or [Expression]"), + crate::entities::common::format_angle(self.taper_angle) + ), ExtrudeStep::Expression => t!("EXTRUDE Enter height expression:").into_owned(), + ExtrudeStep::TaperExpression => t!("EXTRUDE Enter taper angle expression:").into_owned(), } } fn options(&self) -> Vec { @@ -104,6 +155,8 @@ impl CadCommand for ExtrudeCommand { CmdOption::new("Taper angle", "TAPER"), CmdOption::new("Expression", "EXPRESSION"), ], + ExtrudeStep::Path => vec![CmdOption::new("Taper angle", "TAPER")], + ExtrudeStep::Taper => vec![CmdOption::new("Expression", "EXPRESSION")], _ => Vec::new(), } } @@ -171,12 +224,14 @@ impl CadCommand for ExtrudeCommand { ExtrudeStep::Pick | ExtrudeStep::Mode | ExtrudeStep::Height + | ExtrudeStep::Path | ExtrudeStep::Taper | ExtrudeStep::Expression + | ExtrudeStep::TaperExpression ) } fn point_step_accepts_keywords(&self) -> bool { - self.step == ExtrudeStep::Height + matches!(self.step, ExtrudeStep::Height | ExtrudeStep::Path) } fn on_text_input(&mut self, text: &str) -> Option { let value = text.trim(); @@ -194,6 +249,10 @@ impl CadCommand for ExtrudeCommand { } else { return Some(CmdResult::NeedPoint); } + extrude_defaults() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .mode = self.mode; self.step = ExtrudeStep::Pick; Some(CmdResult::NeedPoint) } @@ -208,6 +267,7 @@ impl CadCommand for ExtrudeCommand { return Some(CmdResult::NeedPoint); } if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) { + self.taper_return = ExtrudeStep::Height; self.step = ExtrudeStep::Taper; return Some(CmdResult::NeedPoint); } @@ -219,27 +279,71 @@ impl CadCommand for ExtrudeCommand { .filter(|height| height.is_finite() && height.abs() > 1e-6) .map(|height| self.finish(ExtrudeExtent::Height(height))) } + ExtrudeStep::Path => { + let upper = value.to_ascii_uppercase(); + if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) { + self.taper_return = ExtrudeStep::Path; + self.step = ExtrudeStep::Taper; + } + Some(CmdResult::NeedPoint) + } ExtrudeStep::Taper => { + if "EXPRESSION".starts_with(&value.to_ascii_uppercase()) { + self.step = ExtrudeStep::TaperExpression; + return Some(CmdResult::NeedPoint); + } let angle = crate::entities::common::parse_angle(value)?; if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { return Some(CmdResult::NeedPoint); } self.taper_angle = angle; - self.step = ExtrudeStep::Height; + extrude_defaults() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .taper_angle = angle; + self.step = self.taper_return; Some(CmdResult::NeedPoint) } ExtrudeStep::Expression => crate::app::expr_eval::eval_number(value) .filter(|height| height.is_finite() && height.abs() > 1e-6) .map(|height| self.finish(ExtrudeExtent::Height(height))), + ExtrudeStep::TaperExpression => { + let angle = crate::app::expr_eval::eval_number(value) + .and_then(|value| crate::entities::common::parse_angle(&value.to_string()))?; + if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { + return Some(CmdResult::NeedPoint); + } + self.taper_angle = angle; + extrude_defaults() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .taper_angle = angle; + self.step = self.taper_return; + Some(CmdResult::NeedPoint) + } _ => None, } } fn on_enter(&mut self) -> CmdResult { - if self.step == ExtrudeStep::Pick && !self.handles.is_empty() { - self.step = ExtrudeStep::Height; - CmdResult::NeedPoint - } else { - CmdResult::Cancel + match self.step { + ExtrudeStep::Pick if !self.handles.is_empty() => { + self.step = ExtrudeStep::Height; + CmdResult::NeedPoint + } + ExtrudeStep::Mode => { + self.step = ExtrudeStep::Pick; + CmdResult::NeedPoint + } + ExtrudeStep::Height => self + .last_height + .filter(|height| height.is_finite() && height.abs() > 1e-6) + .map(|height| self.finish(ExtrudeExtent::Height(height))) + .unwrap_or(CmdResult::Cancel), + ExtrudeStep::Taper => { + self.step = self.taper_return; + CmdResult::NeedPoint + } + _ => CmdResult::Cancel, } } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 235b4f48f..043aa0680 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -323,7 +323,30 @@ fn sweep_path_length(value: &SolidHistorySweep) -> Option { use acadrust::entities::EmbeddedEntity; use acadrust::EntityType; - let entity = match value.path_entity.as_ref()? { + let path_entity = value.path_entity.as_ref()?; + if let EmbeddedEntity::Spline(path) = path_entity { + if path.degree == 1 && path.control_points.len() >= 2 && path.weights.is_empty() { + let transform = glam::DMat4::from_cols_array(&value.path_entity_transform); + if !transform.is_finite() || transform.determinant().abs() <= 1e-12 { + return None; + } + return Some( + path.control_points + .windows(2) + .map(|pair| { + let start = transform.transform_point3(glam::DVec3::new( + pair[0].x, pair[0].y, pair[0].z, + )); + let end = transform.transform_point3(glam::DVec3::new( + pair[1].x, pair[1].y, pair[1].z, + )); + end.distance(start) + }) + .sum(), + ); + } + } + let entity = match path_entity { EmbeddedEntity::Line(value) => EntityType::Line(value.clone()), EmbeddedEntity::Arc(value) => EntityType::Arc(value.clone()), EmbeddedEntity::Circle(value) => EntityType::Circle(value.clone()), @@ -1076,7 +1099,7 @@ fn apply_extrusion_geometry_property( } PROP_EXTRUSION_HEIGHT => { let height = crate::entities::common::parse_length(text)?; - if !height.is_finite() || height.abs() <= 1e-9 { + if !height.is_finite() || height <= 1e-9 { return Some(false); } let direction = glam::DVec3::new( @@ -1087,12 +1110,7 @@ fn apply_extrusion_geometry_property( let Some(unit) = direction.try_normalize() else { return Some(false); }; - let sign = if height.signum() == value.end_draft_distance.signum() { - 1.0 - } else { - -1.0 - }; - let updated = unit * height.abs() * sign; + let updated = unit * height; if (updated - direction).length_squared() <= 1e-24 { return Some(false); } diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 0c7b16deb..c8a7d127d 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -3,7 +3,7 @@ use cadkernel::brep::{self, Body}; use cadkernel::geom2d::{Arc, Curve, EllipseArc, Line}; use cadkernel::space::{PlanarCurve, Plane, Vec3}; -use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex}; +use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex, Spline}; use acadrust::objects::{ SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep, }; @@ -229,22 +229,75 @@ pub fn extruded_surface( taper_angle: f64, ) -> Option { let (profile, closed) = extrusion_profile_of(entity)?; - if closed { - brep::extrude_surface_tapered( - profile.plane, - &profile.pieces, - direction, - taper_angle, - ) - } else if taper_angle.abs() <= 1e-12 { - brep::extrude_surface(profile.plane, &profile.pieces, direction) - } else { - None - } + let _ = closed; + brep::extrude_surface_tapered( + profile.plane, + &profile.pieces, + direction, + taper_angle, + ) } -pub fn extruded_along_path(entity: &EntityType, path: &EntityType) -> Option { - swept(entity, path) +pub fn extruded_along_path( + entity: &EntityType, + path: &EntityType, + taper_angle: f64, +) -> Option { + let profile = profile_of(entity)?; + if let EntityType::Polyline3D(path) = path { + let points = path + .vertices + .iter() + .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) + .collect::>(); + if taper_angle.abs() > 1e-12 { + if points.len() != 2 { + return None; + } + return brep::extrude_tapered( + profile.plane, + &profile.pieces, + (Vec3::from(points[1]) - Vec3::from(points[0])).to_array(), + taper_angle, + ); + } + return brep::sweep_along_polyline3d(profile.plane, &profile.pieces, &points); + } + if taper_angle.abs() <= 1e-12 { + return swept(entity, path); + } + let curve = entity_curve(path)?; + let Curve::Line(line) = curve.curve else { + return None; + }; + let direction = Vec3::from(curve.plane.point_at(line.end)) + - Vec3::from(curve.plane.point_at(line.start)); + brep::extrude_tapered( + profile.plane, + &profile.pieces, + direction.to_array(), + taper_angle, + ) +} + +pub fn straight_path_direction(path: &EntityType) -> Option<[f64; 3]> { + if let EntityType::Polyline3D(path) = path { + if path.vertices.len() != 2 { + return None; + } + let start = &path.vertices[0].position; + let end = &path.vertices[1].position; + return Some([end.x - start.x, end.y - start.y, end.z - start.z]); + } + let curve = entity_curve(path)?; + let Curve::Line(line) = curve.curve else { + return None; + }; + Some( + (Vec3::from(curve.plane.point_at(line.end)) + - Vec3::from(curve.plane.point_at(line.start))) + .to_array(), + ) } /// Signed distance of a drag along a profile's normal. @@ -346,13 +399,28 @@ pub fn embedded_path(entity: &EntityType) -> Option { EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())), EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())), EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())), + EntityType::Polyline3D(value) if !value.is_closed() && value.vertices.len() >= 2 => { + let mut spline = Spline::from_control_points( + 1, + value + .vertices + .iter() + .map(|vertex| vertex.position) + .collect(), + ); + spline.flags.linear = true; + spline.flags.planar = false; + Some(EmbeddedEntity::Spline(spline)) + } _ => None, } } fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { - if let Some(embedded) = embedded_path(entity) { - return Some((embedded, glam::DMat4::IDENTITY.to_cols_array())); + if !matches!(entity, EntityType::Polyline3D(_)) { + if let Some(embedded) = embedded_path(entity) { + return Some((embedded, glam::DMat4::IDENTITY.to_cols_array())); + } } let (profile, closed) = extrusion_profile_of(entity)?; let mut polyline = LwPolyline::new(); @@ -406,11 +474,7 @@ pub fn extrusion_history( taper_angle: f64, reference_point: [f64; 3], ) -> Option { - let signed_height = extrusion_profile_of(profile) - .and_then(|(profile, _)| profile.plane.normal()) - .map(|normal| Vec3::from(direction).dot(Vec3::from(normal))) - .filter(|value| value.is_finite()) - .unwrap_or_else(|| Vec3::from(direction).length()); + let signed_height = Vec3::from(direction).length(); let mut base = SolidHistoryNodeBase::new(1); base.transform = glam::DMat4::IDENTITY.to_cols_array(); let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?; @@ -442,6 +506,33 @@ pub fn extrusion_history( })) } +pub fn sweep_history( + profile: &EntityType, + path: &EntityType, + taper_angle: f64, + reference_point: [f64; 3], +) -> Option { + let mut base = SolidHistoryNodeBase::new(1); + base.transform = glam::DMat4::IDENTITY.to_cols_array(); + let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?; + Some(SolidHistoryOperation::Sweep(SolidHistorySweep { + base, + operation_major: 1, + sweep_entity: Some(sweep_entity), + path_entity: Some(embedded_path(path)?), + draft_angle: taper_angle, + scale_factor: 1.0, + sweep_entity_transform, + path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(), + reference_point: Vector3::new( + reference_point[0], + reference_point[1], + reference_point[2], + ), + ..SolidHistorySweep::default() + })) +} + /// Builds a history-backed wall solid along a supported planar curve. pub fn polysolid( entity: &EntityType,