From 1a18fdd0cf936979efdf34b07e70170b4852446f Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:26:29 +0300 Subject: [PATCH] SWEEP: reference messages, 3D polyline profiles and Scale Reference - 3D polyline profiles are recorded as wire bodies, like 3D polyline paths. - Refused sweeps print the modeling error code and "Unable to sweep N selected objects."; a scaled sweep rejects closed and cornered spatial paths with the reference messages and asks for the path again. - Scale > Reference asks for start and end reference lengths (start / end). - Bank stays on for later sweeps until a twist angle is entered. - Refused twist/scale edits in Properties print the modeling error code. Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/solid3d.rs | 45 +++++++++------- src/app/update/command.rs | 14 ++++- src/modules/insert/solid3d_cmds.rs | 73 ++++++++++++++++---------- src/scene/model/sweep_command_model.rs | 59 ++++++++++++++++++--- src/scene/model/sweep_model.rs | 2 +- src/scene/modify.rs | 13 +++++ 6 files changed, 149 insertions(+), 57 deletions(-) diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index ccaa8de3c..958e8dea7 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -497,22 +497,34 @@ impl OpenCADStudio { let mut created_handles = Vec::new(); let mut consumed = Vec::new(); for (handle, profile) in profiles { - let result = path.as_ref().zip(options).and_then(|(path, options)| { - let mut record = sweep_model::sweep_record(&profile, path, options)?; + // A refused sweep reports the modeler error code, as the + // reference does. + let result = (|| -> Result<_, Option> { + let (path, options) = path.as_ref().zip(options).ok_or(None)?; + let mut record = sweep_model::sweep_record(&profile, path, options).ok_or(None)?; record.flags_294_296[0] |= picked_base; let (_, _, closed) = kernel::acis::sweep_profile_geometry( - record.sweep_entity.as_ref()?, + record.sweep_entity.as_ref().ok_or(None)?, record.sweep_entity_transform, ) - .ok()?; + .map_err(|_| None)?; let surface = mode == ExtrudeMode::Surface || !closed; - let body = - kernel::acis::rebuild_sweep_with_mode(&record, surface).ok()?; - Some((body, record, surface)) - }); - let Some((body, record, surface)) = result else { - failed += 1; - continue; + let body = kernel::acis::rebuild_sweep_with_mode(&record, surface).map_err(|error| match error { + kernel::acis::HistoryRebuildError::Refused(why) => Some(sweep_model::sweep_refusal_code(why)), + _ => None, + })?; + Ok((body, record, surface)) + })(); + let (body, record, surface) = match result { + Ok(value) => value, + Err(code) => { + if let Some(code) = code { + self.command_line.push_output(&format!("Modeling Operation Error: +Error Code Number is {code}")); + } + failed += 1; + continue; + } }; let deletes_path = crate::app::delobj_deletes_auxiliary(delete_objects, surface); @@ -550,14 +562,9 @@ impl OpenCADStudio { self.tabs[i].scene.select_entity(*handle, true); } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "SWEEP: created {created} object(s); {failed} source(s) could not be swept.", - created = created_handles.len(), failed = failed - ).as_ref()); - } else { - self.command_line.push_error( - crate::t!("SWEEP: could not sweep the profile along the path.").as_ref(), - ); + } + if failed > 0 { + self.command_line.push_output(&format!("Unable to sweep {failed} selected objects.")); } if let Some(pending) = pending { self.commit_undo_delta(i, pending); diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 870692e7e..2c85ace58 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -3938,11 +3938,21 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if crate::scene::model::solid_history::is_primitive_property( field, ) { - self.tabs[i].scene.apply_solid_history_property( + if !self.tabs[i].scene.apply_solid_history_property( handle, field, &val, - ); + ) { + if let Some(code) = self.tabs[i] + .scene + .solid_history_property_refusal(handle, field, &val) + { + self.command_line.push_output(&format!( + "Modeling Operation Error: +Error Code Number is {code}" + )); + } + } } else if self.tabs[i] .scene .apply_solid_position_property( diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 934fbc432..d2aedee51 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -1606,6 +1606,10 @@ impl CadCommand for RevolveCommand { // ── SWEEP command ────────────────────────────────────────────────────────── +/// Banking stays on for later sweeps until a twist angle is entered, as in +/// the reference. +static SWEEP_BANK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + pub struct SweepCommand { step: SweepStep, profiles: Vec<(Handle, EntityType)>, @@ -1618,6 +1622,7 @@ pub struct SweepCommand { reference_start: Option, reference_length: f64, new_length_start: Option, + end_length: f64, isolines: usize, color: [f32; 4], } @@ -1630,10 +1635,11 @@ enum SweepStep { Alignment, BasePoint, Scale, + /// The start reference length: a distance, or two points. ReferenceLength, ReferenceEnd, + /// The end reference length: a distance, or two points. NewLength, - NewLengthStart, NewLengthEnd, Twist, } @@ -1648,10 +1654,14 @@ impl SweepCommand { preview_key: None, preview_cache: Vec::new(), mode: ExtrudeMode::Solid, - options: SweepOptions::default(), + options: SweepOptions { + bank: SWEEP_BANK.load(std::sync::atomic::Ordering::Relaxed), + ..SweepOptions::default() + }, reference_start: None, reference_length: 1.0, new_length_start: None, + end_length: 1.0, isolines, color, } @@ -1715,10 +1725,13 @@ impl SweepCommand { true } - fn length_anchor(&self) -> DVec3 { - self.reference_start - .or_else(|| self.selection_options().and_then(|options| options.base_point)) - .unwrap_or(DVec3::ZERO) + /// The Reference scale is the start reference length over the end one. + fn set_end_length(&mut self, length: f64) -> bool { + if !length.is_finite() || length <= 1e-12 { + return false; + } + self.end_length = length; + self.set_scale(self.reference_length / length) } } @@ -1746,21 +1759,23 @@ impl CadCommand for SweepCommand { ), SweepStep::ReferenceLength => format!( "{} <{}>:", - t!("SWEEP Specify reference length or first point"), + t!("SWEEP Specify start reference length"), crate::entities::common::format_length(self.reference_length), ), - SweepStep::ReferenceEnd => t!("SWEEP Specify second reference point:").into_owned(), + SweepStep::ReferenceEnd | SweepStep::NewLengthEnd => t!("SWEEP Specify second point:").into_owned(), SweepStep::NewLength => format!( "{} <{}>:", - t!("SWEEP Specify new length or [Points]"), - crate::entities::common::format_length(self.reference_length * self.options.scale), + t!("SWEEP Specify end reference length"), + crate::entities::common::format_length(self.end_length), ), - SweepStep::NewLengthStart => t!("SWEEP Specify first point of new length:").into_owned(), - SweepStep::NewLengthEnd => t!("SWEEP Specify second point of new length:").into_owned(), SweepStep::Twist => format!( "{} <{}>:", t!("SWEEP Specify twist angle or [Bank]"), - crate::entities::common::format_angle(self.options.twist_angle), + if self.options.bank { + "Bank".to_string() + } else { + crate::entities::common::format_angle(self.options.twist_angle) + }, ), } } @@ -1777,7 +1792,6 @@ impl CadCommand for SweepCommand { ], SweepStep::Alignment => vec![CmdOption::new("Yes", "YES"), CmdOption::new("No", "NO")], SweepStep::Scale => vec![CmdOption::new("Reference", "REFERENCE")], - SweepStep::NewLength => vec![CmdOption::new("Points", "POINTS")], SweepStep::Twist => vec![CmdOption::new("Bank", "BANK")], _ => Vec::new(), } @@ -1801,11 +1815,15 @@ impl CadCommand for SweepCommand { .filter(|(hovered, _)| *hovered == handle) .map(|(_, entity)| entity.clone()) }); - if path.as_ref().is_some_and(crate::scene::model::sweep_model::is_sweep_path) { - self.finish(handle) - } else { - CmdResult::NeedPoint + let Some(path) = path.filter(crate::scene::model::sweep_model::is_sweep_path) else { + return CmdResult::NeedPoint; + }; + if (self.options.scale - 1.0).abs() > 1e-12 { + if let Some(message) = crate::scene::model::sweep_model::scaled_sweep_path_refusal(&path) { + return CmdResult::ReportMeasurement(message.to_string()); + } } + self.finish(handle) } fn on_point(&mut self, point: DVec3) -> CmdResult { @@ -1827,15 +1845,12 @@ impl CadCommand for SweepCommand { } } SweepStep::NewLength => { - self.set_scale(point.distance(self.length_anchor()) / self.reference_length); - } - SweepStep::NewLengthStart => { self.new_length_start = Some(point); self.step = SweepStep::NewLengthEnd; } SweepStep::NewLengthEnd => { if let Some(start) = self.new_length_start { - self.set_scale(point.distance(start) / self.reference_length); + self.set_end_length(point.distance(start)); } } _ => {} @@ -1916,10 +1931,8 @@ impl CadCommand for SweepCommand { } } SweepStep::NewLength => { - if "POINTS".starts_with(&keyword) { - self.step = SweepStep::NewLengthStart; - } else if let Some(length) = crate::entities::common::parse_typed_length(value) { - self.set_scale(length / self.reference_length); + if let Some(length) = crate::entities::common::parse_typed_length(value) { + self.set_end_length(length); } else { return None; } @@ -1927,11 +1940,13 @@ impl CadCommand for SweepCommand { SweepStep::Twist => { if "BANK".starts_with(&keyword) { self.options.bank = true; + SWEEP_BANK.store(true, std::sync::atomic::Ordering::Relaxed); self.step = SweepStep::PickPath; } else if let Some(angle) = crate::entities::common::parse_angle(value) { if angle.is_finite() { self.options.twist_angle = angle; self.options.bank = false; + SWEEP_BANK.store(false, std::sync::atomic::Ordering::Relaxed); self.step = SweepStep::PickPath; } } @@ -1947,9 +1962,11 @@ impl CadCommand for SweepCommand { self.step = SweepStep::PickPath; } SweepStep::Mode => self.step = SweepStep::PickProfiles, - SweepStep::Alignment | SweepStep::Scale | SweepStep::Twist - | SweepStep::NewLength => self.step = SweepStep::PickPath, + SweepStep::Alignment | SweepStep::Scale | SweepStep::Twist => self.step = SweepStep::PickPath, SweepStep::ReferenceLength => self.step = SweepStep::NewLength, + SweepStep::NewLength => { + self.set_end_length(self.end_length); + } _ => return CmdResult::Cancel, } CmdResult::NeedPoint diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 142d78e4d..f954561af 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -9,20 +9,25 @@ use kernel::brep::Body; use crate::command::{ExtrudeMode, SweepOptions}; use super::sweep_model::{embedded_path, embedded_revolve_profile}; +/// A 3D polyline profile or path as the reference records it: a wire body. fn embedded_sweep_profile(entity: &EntityType) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> { - if let EntityType::Region(region) = entity { - Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())) - } else { - embedded_revolve_profile(entity) + match entity { + EntityType::Region(region) => Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())), + EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())), + _ => embedded_revolve_profile(entity), } } /// The sweep path as the reference records it: a 3D polyline as a wire /// body, anything else as its embedded entity. fn embedded_sweep_path(path: &EntityType) -> Option { - let EntityType::Polyline3D(value) = path else { - return embedded_path(path); - }; + match path { + EntityType::Polyline3D(value) => polyline_wire(value), + _ => embedded_path(path), + } +} + +fn polyline_wire(value: &codec::entities::Polyline3D) -> Option { let points = value.vertices.iter() .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) .collect::>(); @@ -41,6 +46,46 @@ pub fn is_sweep_profile(entity: &EntityType) -> bool { }) } +/// The modeling error code the reference reports for a refused sweep. +pub fn sweep_refusal_code(refusal: kernel::brep::SweepRefusal) -> u32 { + match refusal { + kernel::brep::SweepRefusal::Scale => 5016, + kernel::brep::SweepRefusal::Twist => 115065, + kernel::brep::SweepRefusal::Bank => 115007, + } +} + +/// Why the reference refuses a path for a scaled sweep: scaling needs an +/// open path, and a spatial path must be smooth. +pub fn scaled_sweep_path_refusal(path: &EntityType) -> Option<&'static str> { + let (closed, smooth) = match crate::entities::curve::entity_curve(path) { + Some(planar) => (planar.curve.is_closed(), true), + None => match path { + EntityType::Polyline3D(value) => { + let points = value.vertices.iter() + .map(|vertex| glam::DVec3::new(vertex.position.x, vertex.position.y, vertex.position.z)) + .collect::>(); + let directions = points.windows(2) + .filter_map(|pair| (pair[1] - pair[0]).try_normalize()) + .collect::>(); + let straight = directions.windows(2).all(|pair| pair[0].dot(pair[1]) >= 1.0 - 1e-9); + (value.is_closed(), straight && !value.is_closed()) + } + EntityType::Spline(value) => (value.flags.closed, value.degree > 1), + _ => (false, true), + }, + }; + if closed { + Some("Cannot use scale option when path curve is closed. +The selected path curve is not valid.") + } else if !smooth { + Some("Path curve must be smooth when using scale option. +The selected path curve is not valid.") + } else { + None + } +} + pub fn is_sweep_path(entity: &EntityType) -> bool { match embedded_path(entity) { Some(codec::entities::EmbeddedEntity::Spline(value)) => { diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index fc51db626..3cb12ce46 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -13,7 +13,7 @@ use codec::EntityType; use crate::entities::curve::entity_curve; pub use super::sweep_command_model::{ - is_sweep_path, is_sweep_profile, sweep_record, sweep_selection_options, swept_surface_entity, swept_with_options, + is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options, }; /// A drawn profile, as the kernel wants it: the plane it lies in and the diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 97dbb38af..45a769e51 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1370,6 +1370,19 @@ impl Scene { self.rebuild_solid_history(handle, operation) } + /// The modeler error code the reference reports when it refuses a sweep + /// property edit (twist or scale along a path with a corner). + pub fn solid_history_property_refusal(&self, handle: Handle, field: &str, value: &str) -> Option { + let mut operation = crate::scene::model::solid_history::primitive_property_operation(&self.document, handle)?; + if !crate::scene::model::solid_history::apply_primitive_property(&mut operation, field, value) { + return None; + } + let codec::objects::SolidHistoryOperation::Sweep(record) = &operation else { + return None; + }; + kernel::acis::sweep_history_refusal(record).map(crate::scene::model::sweep_model::sweep_refusal_code) + } + pub fn apply_solid_position_property( &mut self, handle: Handle,