From 063a1d5be136e6f4dcdf869d76fc895d706a851a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 18:07:00 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20SWEEP=20command=20=E2=80=94=20sweep=20p?= =?UTF-8?q?rofile=20along=20Line/Polyline=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks a profile (closed or open 2D entity) then a path entity (Line or LwPolyline). For Line paths uses tsweep along the line vector; for Polyline paths uses start→end as translation direction. Closed profiles produce a capped Solid; open profiles a Shell. Co-Authored-By: Claude Sonnet 4.6 --- src/app/cmd_result.rs | 112 +++++++++++++++++++++++++++++ src/app/commands.rs | 9 +++ src/command/mod.rs | 6 ++ src/modules/insert/solid3d_cmds.rs | 47 ++++++++++++ 4 files changed, 174 insertions(+) diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 004e2867d..8b8a8ab88 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -995,6 +995,118 @@ impl H7CAD { self.restore_pre_cmd_tangent(); } + // ── SWEEP ────────────────────────────────────────────────────── + CmdResult::SweepEntity { profile_handle, path_handle, color } => { + use crate::entities::traits::EntityTypeOps; + use crate::scene::acad_to_truck::TruckObject; + use crate::scene::truck_tess; + use crate::modules::insert::solid3d_cmds::empty_solid3d; + use truck_modeling::builder; + use truck_modeling::{Point3, Rad, Vector3 as TruckVec3}; + + let profile_ent = self.tabs[i].scene.document.get_entity(profile_handle).cloned(); + let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned(); + + let result = profile_ent.zip(path_ent).and_then(|(prof_e, path_e)| { + let prof_truck = prof_e.to_truck_entity(&self.tabs[i].scene.document)?; + let path_truck = path_e.to_truck_entity(&self.tabs[i].scene.document)?; + + // Profile must be a wire (closed or open). + let profile_wire: truck_modeling::Wire = match prof_truck.object { + TruckObject::Contour(w) => w, + TruckObject::Curve(e) => std::iter::once(e).collect(), + _ => return None, + }; + + // Path determines the sweep operation. + let mesh = match path_truck.object { + // Linear path: translate profile along the line direction. + TruckObject::Curve(edge) => { + let p_start = edge.front().point(); + let p_end = edge.back().point(); + let dir = TruckVec3::new( + p_end.x - p_start.x, + p_end.y - p_start.y, + p_end.z - p_start.z, + ); + // Try to build a face from the profile; if it's a closed + // wire we get a Solid, otherwise a Shell. + if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) { + let solid = builder::tsweep(&face, dir); + match truck_tess::tessellate_solid(&solid) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => + Some(crate::scene::mesh_model::MeshModel { + name: String::new(), verts, normals, indices, color, selected: false, + }), + _ => None, + } + } else { + let shell = builder::tsweep(&profile_wire, dir); + match truck_tess::tessellate_shell(&shell) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => + Some(crate::scene::mesh_model::MeshModel { + name: String::new(), verts, normals, indices, color, selected: false, + }), + _ => None, + } + } + } + + // Contour path (polyline): sweep along the polyline using the + // first edge's direction as approximation (multi-segment sweep + // requires NURBS deformation — not supported here). + TruckObject::Contour(path_wire) => { + // Use start→end of the whole wire as translation vector. + let p_start = path_wire.front_vertex()?.point(); + let p_end = path_wire.back_vertex()?.point(); + let dir = TruckVec3::new( + p_end.x - p_start.x, + p_end.y - p_start.y, + p_end.z - p_start.z, + ); + if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) { + let solid = builder::tsweep(&face, dir); + match truck_tess::tessellate_solid(&solid) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => + Some(crate::scene::mesh_model::MeshModel { + name: String::new(), verts, normals, indices, color, selected: false, + }), + _ => None, + } + } else { + let shell = builder::tsweep(&profile_wire, dir); + match truck_tess::tessellate_shell(&shell) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => + Some(crate::scene::mesh_model::MeshModel { + name: String::new(), verts, normals, indices, color, selected: false, + }), + _ => None, + } + } + } + + _ => None, + }; + mesh + }); + + if let Some(mut mesh) = result { + self.push_undo_snapshot(i, "SWEEP"); + let new_entity = empty_solid3d(); + let new_handle = self.tabs[i].scene.add_entity(new_entity); + mesh.name = format!("{}", new_handle.value()); + self.tabs[i].scene.meshes.insert(new_handle, mesh); + self.tabs[i].dirty = true; + self.command_line.push_output("SWEEP: solid created."); + } else { + self.command_line.push_error("SWEEP: could not sweep profile along path. Use a closed 2D profile and a Line or Polyline path."); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } + CmdResult::HatcheditApply { handle, name, scale, angle } => { if let Some(mut model) = self.tabs[i].scene.hatches.get(&handle).cloned() { // Update model fields diff --git a/src/app/commands.rs b/src/app/commands.rs index fe27b0a4f..91fb3b174 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3457,6 +3457,15 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + // ── SWEEP ────────────────────────────────────────────────────── + "SWEEP" => { + use crate::modules::insert::solid3d_cmds::SweepCommand; + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + let cmd = SweepCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + // ── OBJ import ─────────────────────────────────────────────── "IMPORTOBJ"|"OBJIMPORT" => { return Task::done(Message::ObjImport); diff --git a/src/command/mod.rs b/src/command/mod.rs index 4357546b7..cb846a38b 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -153,6 +153,12 @@ pub enum CmdResult { angle_deg: f32, color: [f32; 4], }, + /// Sweep the profile entity `profile_handle` along `path_handle`. + SweepEntity { + profile_handle: Handle, + path_handle: Handle, + color: [f32; 4], + }, /// INSERT landed on a block that has AttributeDefinitions. /// The host should look up the attdefs for `block_name` from the document /// and call `attreq_set_attdefs()` on the command, then loop on text input. diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 68b020843..7b5e27a34 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -368,6 +368,53 @@ impl RevolveCommand { } } +// ── SWEEP command ────────────────────────────────────────────────────────── + +pub struct SweepCommand { + step: SweepStep, + profile_handle: acadrust::Handle, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum SweepStep { PickProfile, PickPath } + +impl SweepCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { step: SweepStep::PickProfile, profile_handle: acadrust::Handle::NULL, color } + } +} + +impl CadCommand for SweepCommand { + fn name(&self) -> &'static str { "SWEEP" } + fn prompt(&self) -> String { + match self.step { + SweepStep::PickProfile => "SWEEP Select profile to sweep:".into(), + SweepStep::PickPath => "SWEEP Select path (Line, Arc, LwPolyline):".into(), + } + } + fn needs_entity_pick(&self) -> bool { true } + fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + match self.step { + SweepStep::PickProfile => { + self.profile_handle = handle; + self.step = SweepStep::PickPath; + CmdResult::NeedPoint + } + SweepStep::PickPath => { + CmdResult::SweepEntity { + profile_handle: self.profile_handle, + path_handle: handle, + color: self.color, + } + } + } + } + fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} + // ── Placeholder Solid3D entity construction ──────────────────────────────── /// Create a minimal Solid3D entity with empty ACIS data (placeholder only).