From fea64a1e91b73cfcdf94408ccec3996993e22468 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:48:53 +0300 Subject: [PATCH] Complete WEDGE creation and properties parity --- src/app/properties.rs | 12 +- src/modules/model/primitive_cmd.rs | 221 +++++++++++++++++++---------- src/scene/model/solid_history.rs | 41 +++--- src/scene/modify.rs | 8 +- 4 files changed, 181 insertions(+), 101 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index cf4a5d093..7f6be66a5 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -441,14 +441,14 @@ impl OpenCADStudio { let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane); let entity = &display_entity; let group_names = self.tabs[i].scene.group_names_for_entity(handle); - let box_primitive = - crate::scene::model::solid_history::is_box_primitive( + let rectangular_primitive = + crate::scene::model::solid_history::is_rectangular_primitive( &self.tabs[i].scene.document, handle, ); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); - if box_primitive { + if rectangular_primitive { sections.retain(|section| { !section.props.iter().any(|property| { property.field.starts_with("acis_") @@ -620,7 +620,7 @@ impl OpenCADStudio { } } - if !box_primitive && matches!( + if !rectangular_primitive && matches!( entity, acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Region(_) @@ -694,7 +694,7 @@ impl OpenCADStudio { } } - if !box_primitive { + if !rectangular_primitive { sections.extend(crate::entities::object_data::sections( &self.tabs[i].scene.document, &self.tabs[i].scene.object_data_cache, @@ -2386,7 +2386,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 cfaa65ca2..7c9fa7ed8 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -5,12 +5,15 @@ use acadrust::objects::SolidHistoryOperation; use acadrust::EntityType; use cadkernel::brep::Body; use glam::DVec3; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::command::{CadCommand, CmdOption, CmdResult, 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 { @@ -63,7 +66,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 { @@ -116,19 +122,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) @@ -268,19 +288,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)) } @@ -319,48 +344,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(); @@ -440,6 +485,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, @@ -482,7 +530,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 @@ -501,7 +549,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), @@ -511,7 +559,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 => t!("%{n} Specify height or [2Point]:", n = n), + BoxStep::Height => t!( + "%{n} Specify height or [2Point] <%{height}>:", + n = n, + height = format!("{:.4}", self.default_height()) + ), BoxStep::HeightFirstPoint => { t!("%{n} Specify first point for height:", n = n) } @@ -540,7 +592,7 @@ impl CadCommand for PrimitiveCommand { } fn options(&self) -> Vec { - if self.shape != Shape::Box { + if !self.shape.rectangular() { return Vec::new(); } match self.box_step { @@ -555,7 +607,7 @@ impl CadCommand for PrimitiveCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { - if self.shape == Shape::Box { + if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { return CmdResult::NeedPoint; @@ -567,12 +619,23 @@ 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 height.abs() >= 1e-6 { + self.commit(height) + } else { + CmdResult::NeedPoint + } + } + BoxStep::CubeSize => { + let delta = local - self.pts[0]; + self.set_box_cube( + delta.x.hypot(delta.y), + delta.y.atan2(delta.x), + ) + .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)) { @@ -632,8 +695,12 @@ impl CadCommand for PrimitiveCommand { } fn on_enter(&mut self) -> CmdResult { - if self.shape == Shape::Box { - return CmdResult::Cancel; + if self.shape.rectangular() { + return if self.box_step == BoxStep::Height { + self.commit(self.default_height()) + } else { + CmdResult::Cancel + }; } if self.height_step { let h = self.default_height(); @@ -647,7 +714,7 @@ impl CadCommand for PrimitiveCommand { } fn wants_text_input(&self) -> bool { - if self.shape == Shape::Box { + if self.shape.rectangular() { matches!( self.box_step, BoxStep::FirstCorner @@ -663,7 +730,7 @@ impl CadCommand for PrimitiveCommand { } fn point_step_accepts_keywords(&self) -> bool { - self.shape == Shape::Box + self.shape.rectangular() && matches!( self.box_step, BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height @@ -671,7 +738,7 @@ impl CadCommand for PrimitiveCommand { } fn on_text_input(&mut self, raw: &str) -> Option { - if self.shape == Shape::Box { + if self.shape.rectangular() { let token = raw.trim(); let upper = token.to_uppercase(); return match self.box_step { @@ -693,7 +760,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) @@ -722,7 +789,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; @@ -750,18 +817,25 @@ 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 = delta.x.hypot(delta.y).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_origin = Some(self.pts[0]); preview.box_length = Some(size); preview.box_width = Some(size); - preview.box_preview(size) + preview.box_angle = delta.y.atan2(delta.x); + preview.box_width_sign = 1.0; + if 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", @@ -771,7 +845,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; @@ -779,9 +853,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", @@ -806,7 +880,7 @@ impl CadCommand for PrimitiveCommand { fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; - 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, @@ -827,13 +901,16 @@ impl CadCommand for PrimitiveCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { - 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(delta.x.hypot(delta.y)) + } 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 a7c05f7be..2db5e0673 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -63,19 +63,19 @@ 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(_)) ) } 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], ), @@ -83,10 +83,11 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option } } -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, @@ -106,7 +107,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(), @@ -183,12 +184,12 @@ pub fn primitive_properties( return Vec::new(); }; let props = match operation { - SolidHistoryOperation::Box(value) => return box_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::Box(value) => { + return rectangular_properties(document, handle, value, "Box") + } + SolidHistoryOperation::Wedge(value) => { + return rectangular_properties(document, handle, value, "Wedge") + } 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), @@ -287,7 +288,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, @@ -360,8 +361,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; } } @@ -376,7 +381,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; }; @@ -411,12 +416,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..03158bd9c 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,7 +791,11 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_)); + let rectangular_primitive = matches!( + operation, + acadrust::objects::SolidHistoryOperation::Box(_) + | acadrust::objects::SolidHistoryOperation::Wedge(_) + ); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -799,7 +803,7 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if box_primitive { + if rectangular_primitive { let _ = crate::scene::model::solid_history::apply_history_choice( &mut self.document, handle,