diff --git a/src/app/properties.rs b/src/app/properties.rs index 1eeaa930d..11ccbb29e 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2399,7 +2399,7 @@ impl OpenCADStudio { annotation_scale_handle, ); let mut entity_grips = dispatch::grips(contextual.as_ref()); - if crate::scene::model::solid_history::is_box_primitive( + if crate::scene::model::solid_history::is_rectangular_primitive( &self.tabs[i].scene.document, handle, ) { diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 0abfbeec7..df54a3400 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -10,12 +10,15 @@ use cadkernel::geom2d::{ }; use glam::DVec3; use std::sync::{Mutex, OnceLock}; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; +static LAST_WEDGE_HEIGHT: AtomicU64 = AtomicU64::new(0); + /// Which primitive a `PrimitiveCommand` builds. #[derive(Clone, Copy, PartialEq, Eq)] pub enum Shape { @@ -178,7 +181,10 @@ impl Shape { /// True for footprints picked as a centre + radius (round shapes); false /// for corner-to-corner footprints (box/wedge). fn radial(self) -> bool { - !matches!(self, Shape::Box | Shape::Wedge) + !self.rectangular() + } + fn rectangular(self) -> bool { + matches!(self, Shape::Box | Shape::Wedge) } /// Whether a height value is collected after the footprint. fn needs_height(self) -> bool { @@ -275,19 +281,33 @@ impl PrimitiveCommand { /// A reasonable default height when the user just presses Enter. fn default_height(&self) -> f64 { - match self.shape { - Shape::Box | Shape::Wedge => { - let d = self.pts[1] - self.pts[0]; - d.x.abs().max(d.y.abs()) + if self.shape == Shape::Wedge { + let bits = LAST_WEDGE_HEIGHT.load(Ordering::Relaxed); + if bits != 0 { + let height = f64::from_bits(bits); + if height.is_finite() && height >= 1e-6 { + return height; + } } - _ => (self.pts[1] - self.pts[0]).length(), } - .max(1.0) + let height = match self.shape { + Shape::Box | Shape::Wedge => match (self.box_length, self.box_width) { + (Some(length), Some(width)) => length.max(width), + _ if self.pts.len() >= 2 => { + let d = self.pts[1] - self.pts[0]; + d.x.abs().max(d.y.abs()) + } + _ => 1.0, + }, + _ if self.pts.len() >= 2 => (self.pts[1] - self.pts[0]).length(), + _ => 1.0, + }; + height.max(1.0) } fn cursor_height(&self, point: DVec3) -> f64 { let height = self.plane.to_local(point).z - self.pts[0].z; - if self.shape == Shape::Box { + if self.shape.rectangular() { height } else { height.max(1e-6) @@ -427,19 +447,24 @@ impl PrimitiveCommand { true } - fn set_box_cube(&mut self, size: f64) -> Option { + fn set_box_cube(&mut self, size: f64, angle: f64) -> Option { let size = size.abs(); - if !size.is_finite() || size < 1e-6 { + if !size.is_finite() || !angle.is_finite() || size < 1e-6 { return None; } + let (sin, cos) = angle.sin_cos(); + let x_axis = DVec3::new(cos, sin, 0.0); + let y_axis = DVec3::new(-sin, cos, 0.0); let origin = if self.box_centered { - self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0) + self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5 } else { self.pts[0] }; self.box_origin = Some(origin); self.box_length = Some(size); self.box_width = Some(size); + self.box_angle = angle; + self.box_width_sign = 1.0; Some(self.commit(size)) } @@ -478,48 +503,68 @@ impl PrimitiveCommand { true } - fn box_preview(&self, height: f64) -> Option { + fn rectangular_preview(&self, height: f64) -> Option { let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; - let base = [ - origin, - origin + x_axis * length, - origin + x_axis * length + y_axis * width, - origin + y_axis * width, - ]; - let top = base.map(|point| point + DVec3::Z * height); let mut points = Vec::new(); - push_loop(&mut points, &base); - push_loop(&mut points, &top); - for index in 0..4 { - push_segment(&mut points, base[index], top[index]); + if self.shape == Shape::Wedge { + let near = [ + origin, + origin + x_axis * length, + origin + DVec3::Z * height, + ]; + let far = near.map(|point| point + y_axis * width); + push_loop(&mut points, &near); + push_loop(&mut points, &far); + for index in 0..3 { + push_segment(&mut points, near[index], far[index]); + } + } else { + let base = [ + origin, + origin + x_axis * length, + origin + x_axis * length + y_axis * width, + origin + y_axis * width, + ]; + let top = base.map(|point| point + DVec3::Z * height); + push_loop(&mut points, &base); + push_loop(&mut points, &top); + for index in 0..4 { + push_segment(&mut points, base[index], top[index]); + } } Some(self.place_preview(wire("primitive_height_preview", points))) } + fn build_wedge(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> { + use crate::scene::model::solid_history; + + let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; + let base = solid_model::wedge_solid([0.0; 3], length, width, height)?; + let placed = solid_model::placed( + &base, + x_axis.to_array(), + y_axis.to_array(), + DVec3::Z.to_array(), + origin.to_array(), + )?; + Some(( + placed, + solid_history::wedge_op( + self.history_transform_axes(origin, x_axis, y_axis), + length, + width, + height, + ), + )) + } + /// Build the persistent kernel body and its history node. fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> { use crate::scene::model::solid_history; let (solid, history) = match self.shape { Shape::Box => return self.build_box(height), - Shape::Wedge => { - let (a, b) = (self.pts[0], self.pts[1]); - let length = (b.x - a.x).abs(); - let width = (b.y - a.y).abs(); - if length < 1e-6 || width < 1e-6 || height < 1e-6 { - return None; - } - let origin = [a.x.min(b.x), a.y.min(b.y), a.z]; - ( - solid_model::wedge_solid(origin, length, width, height), - solid_history::wedge_op( - self.history_transform(DVec3::from_array(origin)), - length, - width, - height, - ), - ) - } + Shape::Wedge => return self.build_wedge(height), Shape::Cylinder | Shape::Cone => { let c = self.pts[0]; let r = (self.pts[1] - c).length(); @@ -599,6 +644,9 @@ impl PrimitiveCommand { fn commit(&self, height: f64) -> CmdResult { match self.build(height) { Some((solid, history)) => { + if self.shape == Shape::Wedge { + LAST_WEDGE_HEIGHT.store(height.abs().to_bits(), Ordering::Relaxed); + } // Place the local body on the working plane. let placed = solid_model::placed( &solid, @@ -641,7 +689,7 @@ impl CadCommand for PrimitiveCommand { } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { - let constrained = if self.shape == Shape::Box { + let constrained = if self.shape.rectangular() { self.box_step == BoxStep::Height } else { self.height_step @@ -695,7 +743,7 @@ impl CadCommand for PrimitiveCommand { fn prompt(&self) -> String { let n = self.shape.name(); - if self.shape == Shape::Box { + if self.shape.rectangular() { return match self.box_step { BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n), BoxStep::CenterPoint => t!("%{n} Specify center point:", n = n), @@ -705,6 +753,11 @@ impl CadCommand for PrimitiveCommand { BoxStep::CubeSize => t!("%{n} Specify cube length:", n = n), BoxStep::Length => t!("%{n} Specify length:", n = n), BoxStep::Width => t!("%{n} Specify width:", n = n), + BoxStep::Height if self.shape == Shape::Wedge => t!( + "%{n} Specify height or [2Point] <%{height}>:", + n = n, + height = format!("{:.4}", self.default_height()) + ), BoxStep::Height => t!("%{n} Specify height or [2Point]:", n = n), BoxStep::HeightFirstPoint => { t!("%{n} Specify first point for height:", n = n) @@ -788,7 +841,7 @@ impl CadCommand for PrimitiveCommand { _ => Vec::new(), }; } - if self.shape != Shape::Box { + if !self.shape.rectangular() { return Vec::new(); } match self.box_step { @@ -846,7 +899,7 @@ impl CadCommand for PrimitiveCommand { SphereStep::TtrFirst | SphereStep::TtrSecond { .. } => CmdResult::NeedPoint, }; } - if self.shape == Shape::Box { + if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { return CmdResult::NeedPoint; @@ -858,12 +911,25 @@ impl CadCommand for PrimitiveCommand { CmdResult::NeedPoint } BoxStep::OppositeCorner => { - self.set_box_corner_footprint(local); - CmdResult::NeedPoint + let height = local.z - self.pts[0].z; + if !self.set_box_corner_footprint(local) { + CmdResult::NeedPoint + } else if self.shape == Shape::Wedge && height.abs() >= 1e-6 { + self.commit(height) + } else { + CmdResult::NeedPoint + } + } + BoxStep::CubeSize => { + let delta = local - self.pts[0]; + let (size, angle) = if self.shape == Shape::Wedge { + (delta.x.hypot(delta.y), delta.y.atan2(delta.x)) + } else { + (delta.length(), 0.0) + }; + self.set_box_cube(size, angle) + .unwrap_or(CmdResult::NeedPoint) } - BoxStep::CubeSize => self - .set_box_cube((local - self.pts[0]).length()) - .unwrap_or(CmdResult::NeedPoint), BoxStep::Length => { let delta = local - self.pts[0]; if !self.set_box_length(delta.x.hypot(delta.y), delta.y.atan2(delta.x)) { @@ -937,8 +1003,12 @@ impl CadCommand for PrimitiveCommand { _ => CmdResult::Cancel, }; } - if self.shape == Shape::Box { - return CmdResult::Cancel; + if self.shape.rectangular() { + return if self.shape == Shape::Wedge && self.box_step == BoxStep::Height { + self.commit(self.default_height()) + } else { + CmdResult::Cancel + }; } if self.height_step { let h = self.default_height(); @@ -961,7 +1031,7 @@ impl CadCommand for PrimitiveCommand { | SphereStep::TtrRadius { .. } ); } - if self.shape == Shape::Box { + if self.shape.rectangular() { matches!( self.box_step, BoxStep::FirstCorner @@ -980,7 +1050,7 @@ impl CadCommand for PrimitiveCommand { if self.shape == Shape::Sphere { return matches!(self.sphere_step, SphereStep::Center | SphereStep::Radius(_)); } - self.shape == Shape::Box + self.shape.rectangular() && matches!( self.box_step, BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height @@ -1026,7 +1096,7 @@ impl CadCommand for PrimitiveCommand { _ => None, }; } - if self.shape == Shape::Box { + if self.shape.rectangular() { let token = raw.trim(); let upper = token.to_uppercase(); return match self.box_step { @@ -1048,7 +1118,7 @@ impl CadCommand for PrimitiveCommand { Some(CmdResult::NeedPoint) } BoxStep::CubeSize => crate::entities::common::parse_typed_length(token) - .and_then(|value| self.set_box_cube(value)), + .and_then(|value| self.set_box_cube(value, 0.0)), BoxStep::Length => { let value = crate::entities::common::parse_typed_length(token)?; self.set_box_length(value, 0.0) @@ -1114,7 +1184,7 @@ impl CadCommand for PrimitiveCommand { if self.pts.is_empty() { return None; } - if self.shape == Shape::Box { + if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { return None; @@ -1142,18 +1212,38 @@ impl CadCommand for PrimitiveCommand { Some(self.place_preview(footprint_wire(Shape::Box, &points))) } BoxStep::CubeSize => { - let size = (local - self.pts[0]).length().max(1e-6); - let mut preview = PrimitiveCommand::new("BOX"); + let delta = local - self.pts[0]; + let size = if self.shape == Shape::Wedge { + delta.x.hypot(delta.y) + } else { + delta.length() + } + .max(1e-6); + let mut preview = PrimitiveCommand::new(self.shape.name()); preview.plane = self.plane; preview.pts = self.pts.clone(); - preview.box_origin = Some(if self.box_centered { - self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0) - } else { - self.pts[0] - }); preview.box_length = Some(size); preview.box_width = Some(size); - preview.box_preview(size) + preview.box_width_sign = 1.0; + if self.shape == Shape::Wedge { + preview.box_origin = Some(self.pts[0]); + preview.box_angle = delta.y.atan2(delta.x); + } else { + preview.box_origin = Some(if self.box_centered { + self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0) + } else { + self.pts[0] + }); + } + if self.shape == Shape::Wedge && self.box_centered { + let (sin, cos) = preview.box_angle.sin_cos(); + let x_axis = DVec3::new(cos, sin, 0.0); + let y_axis = DVec3::new(-sin, cos, 0.0); + preview.box_origin = Some( + self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5, + ); + } + preview.rectangular_preview(size) } BoxStep::Length => Some(self.place_preview(wire( "primitive_preview", @@ -1163,7 +1253,7 @@ impl CadCommand for PrimitiveCommand { let delta = local - self.pts[0]; let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0); let signed = delta.dot(normal); - let mut preview = PrimitiveCommand::new("BOX"); + let mut preview = PrimitiveCommand::new(self.shape.name()); preview.plane = self.plane; preview.pts = self.pts.clone(); preview.box_origin = self.box_origin; @@ -1171,9 +1261,9 @@ impl CadCommand for PrimitiveCommand { preview.box_width = Some(signed.abs().max(1e-6)); preview.box_angle = self.box_angle; preview.box_width_sign = if signed < 0.0 { -1.0 } else { 1.0 }; - preview.box_preview(1e-6) + preview.rectangular_preview(1e-6) } - BoxStep::Height => self.box_preview(self.cursor_height(pt)), + BoxStep::Height => self.rectangular_preview(self.cursor_height(pt)), BoxStep::HeightSecondPoint => self.box_height_anchor.map(|first| { self.place_preview(wire( "primitive_preview", @@ -1215,7 +1305,7 @@ impl CadCommand for PrimitiveCommand { }); } - let role = if self.shape == Shape::Box { + let role = if self.shape.rectangular() { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, BoxStep::Width => DynRole::Width, @@ -1247,13 +1337,20 @@ impl CadCommand for PrimitiveCommand { _ => None, }; } - if self.shape == Shape::Box { + if self.shape.rectangular() { let local = self.plane.to_local(cursor); if !local.is_finite() { return None; } return match self.box_step { - BoxStep::CubeSize | BoxStep::Length => Some((local - self.pts[0]).length()), + BoxStep::CubeSize | BoxStep::Length => { + let delta = local - self.pts[0]; + Some(if self.shape == Shape::Wedge { + delta.x.hypot(delta.y) + } else { + delta.length() + }) + } BoxStep::Width => { let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0); Some((local - self.pts[0]).dot(normal).abs()) diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 49f697ff6..57fe02b0b 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -64,13 +64,13 @@ fn history_flags( Some((history.record_history, history.show_history)) } -pub fn is_box_primitive( +pub fn is_rectangular_primitive( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> bool { matches!( document.solid_history_operation(handle), - Some(SolidHistoryOperation::Box(_)) + Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Wedge(_)) ) } @@ -80,13 +80,17 @@ pub fn has_specialized_primitive_properties( ) -> bool { matches!( document.solid_history_operation(handle), - Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Sphere(_)) + Some( + SolidHistoryOperation::Box(_) + | SolidHistoryOperation::Wedge(_) + | SolidHistoryOperation::Sphere(_) + ) ) } pub fn reference_point(operation: &SolidHistoryOperation) -> Option { match operation { - SolidHistoryOperation::Box(value) => world_point( + SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => world_point( value.base.transform, [value.length * 0.5, value.width * 0.5, 0.0], ), @@ -162,10 +166,11 @@ fn sphere_properties( ] } -fn box_properties( +fn rectangular_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, value: &SolidHistoryBox, + solid_type: &str, ) -> Vec { let Some(position) = world_point( value.base.transform, @@ -185,7 +190,7 @@ fn box_properties( Property { label: t!("Solid type").into_owned(), field: "solid_history_type", - value: PropValue::ReadOnly(t!("Box").into_owned()), + value: PropValue::ReadOnly(t!(solid_type).into_owned()), }, crate::entities::common::edit_prop( t!("Position X").as_ref(), @@ -262,15 +267,15 @@ pub fn primitive_properties( return Vec::new(); }; let props = match operation { - SolidHistoryOperation::Box(value) => return box_properties(document, handle, value), + SolidHistoryOperation::Box(value) => { + return rectangular_properties(document, handle, value, "Box") + } + SolidHistoryOperation::Wedge(value) => { + return rectangular_properties(document, handle, value, "Wedge") + } SolidHistoryOperation::Sphere(value) => { return sphere_properties(document, handle, value) } - SolidHistoryOperation::Wedge(value) => vec![ - history_prop(t!("Length").as_ref(), PROP_LENGTH, value.length), - history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width), - history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), - ], SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![ history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius), history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), @@ -365,7 +370,7 @@ pub fn apply_history_choice( before != (history.record_history, history.show_history) } -fn apply_box_geometry_property( +fn apply_rectangular_geometry_property( value: &mut SolidHistoryBox, field: &str, text: &str, @@ -468,8 +473,12 @@ pub fn apply_primitive_property( field: &str, value: &str, ) -> bool { - if let SolidHistoryOperation::Box(box_value) = operation { - if let Some(applied) = apply_box_geometry_property(box_value, field, value) { + if let SolidHistoryOperation::Box(rectangular_value) + | SolidHistoryOperation::Wedge(rectangular_value) = operation + { + if let Some(applied) = + apply_rectangular_geometry_property(rectangular_value, field, value) + { return applied; } } @@ -489,7 +498,7 @@ pub fn apply_primitive_property( return false; } let positive = || (number > 0.0).then_some(number); - if let SolidHistoryOperation::Box(value) = operation { + if let SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) = operation { let Some(next) = positive() else { return false; }; @@ -524,12 +533,6 @@ pub fn apply_primitive_property( return true; } match operation { - SolidHistoryOperation::Wedge(value) => match field { - PROP_LENGTH => value.length = positive().unwrap_or(value.length), - PROP_WIDTH => value.width = positive().unwrap_or(value.width), - PROP_HEIGHT => value.height = positive().unwrap_or(value.height), - _ => return false, - }, SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field { PROP_RADIUS => { let Some(radius) = positive() else { diff --git a/src/scene/modify.rs b/src/scene/modify.rs index f376f9307..505bba18e 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,7 +791,6 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_)); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -799,14 +798,6 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if box_primitive { - let _ = crate::scene::model::solid_history::apply_history_choice( - &mut self.document, - handle, - crate::scene::model::solid_history::PROP_HISTORY, - "None", - ); - } self.sync_solid_reference_point(handle); true }