feat: LOFT command — ruled-surface loft through multiple cross-sections
Picks 2+ profile entities (closed or open wires), builds ruled shells between consecutive pairs via builder::try_wire_homotopy, caps closed ends with planar faces, and tessellates the result into a Solid3D mesh. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
063a1d5be1
commit
1d349c53af
4 changed files with 126 additions and 0 deletions
|
|
@ -1107,6 +1107,75 @@ impl H7CAD {
|
|||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
|
||||
// ── LOFT ───────────────────────────────────────────────────────
|
||||
CmdResult::LoftEntities { handles, 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;
|
||||
|
||||
// Collect wires from each profile.
|
||||
let mut wires: Vec<truck_modeling::Wire> = Vec::new();
|
||||
for h in &handles {
|
||||
if let Some(ent) = self.tabs[i].scene.document.get_entity(*h).cloned() {
|
||||
if let Some(te) = ent.to_truck_entity(&self.tabs[i].scene.document) {
|
||||
let wire = match te.object {
|
||||
TruckObject::Contour(w) => Some(w),
|
||||
TruckObject::Curve(e) => Some(std::iter::once(e).collect()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(w) = wire { wires.push(w); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: Option<crate::scene::mesh_model::MeshModel> = (|| {
|
||||
if wires.len() < 2 { return None; }
|
||||
|
||||
// Build ruled shells between consecutive profile pairs.
|
||||
let mut all_faces: Vec<truck_modeling::Face> = Vec::new();
|
||||
|
||||
for pair in wires.windows(2) {
|
||||
let shell = builder::try_wire_homotopy(&pair[0], &pair[1]).ok()?;
|
||||
for face in shell.into_iter() { all_faces.push(face); }
|
||||
}
|
||||
|
||||
// Cap the first and last profiles if they are closed.
|
||||
if let Ok(cap) = builder::try_attach_plane(&[wires.first()?.clone()]) {
|
||||
all_faces.push(cap);
|
||||
}
|
||||
if let Ok(cap) = builder::try_attach_plane(&[wires.last()?.clone()]) {
|
||||
all_faces.push(cap);
|
||||
}
|
||||
|
||||
let shell = truck_modeling::Shell::from(all_faces);
|
||||
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,
|
||||
}
|
||||
})();
|
||||
|
||||
if let Some(mut mesh) = result {
|
||||
self.push_undo_snapshot(i, "LOFT");
|
||||
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(&format!("LOFT: solid created from {} profiles.", handles.len()));
|
||||
} else {
|
||||
self.command_line.push_error("LOFT: could not loft profiles. Ensure sections have the same edge count and are compatible.");
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3466,6 +3466,15 @@ impl H7CAD {
|
|||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
// ── LOFT ───────────────────────────────────────────────────────
|
||||
"LOFT" => {
|
||||
use crate::modules::insert::solid3d_cmds::LoftCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = LoftCommand::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);
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ pub enum CmdResult {
|
|||
path_handle: Handle,
|
||||
color: [f32; 4],
|
||||
},
|
||||
/// Loft through a series of profile entities.
|
||||
LoftEntities {
|
||||
handles: Vec<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.
|
||||
|
|
|
|||
|
|
@ -415,6 +415,49 @@ impl CadCommand for SweepCommand {
|
|||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
}
|
||||
|
||||
// ── LOFT command ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct LoftCommand {
|
||||
profiles: Vec<acadrust::Handle>,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
impl LoftCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
Self { profiles: Vec::new(), color }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for LoftCommand {
|
||||
fn name(&self) -> &'static str { "LOFT" }
|
||||
fn prompt(&self) -> String {
|
||||
if self.profiles.is_empty() {
|
||||
"LOFT Select first cross-section:".into()
|
||||
} else {
|
||||
format!("LOFT Select next cross-section ({} selected, Enter to finish):", self.profiles.len())
|
||||
}
|
||||
}
|
||||
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; }
|
||||
// Avoid duplicate picks.
|
||||
if !self.profiles.contains(&handle) {
|
||||
self.profiles.push(handle);
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn wants_text_input(&self) -> bool { self.profiles.len() >= 2 }
|
||||
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> { None }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.profiles.len() < 2 {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::LoftEntities { handles: self.profiles.clone(), color: self.color }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Placeholder Solid3D entity construction ────────────────────────────────
|
||||
|
||||
/// Create a minimal Solid3D entity with empty ACIS data (placeholder only).
|
||||
|
|
|
|||
Loading…
Reference in a new issue