From 549ae3845955a8d6c8ebcbf913dc07d1ee62550d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 12:55:28 +0300 Subject: [PATCH] feat: BOX, SPHERE, CYLINDER primitives + EXTRUDE / REVOLVE commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOX, SPHERE, and CYLINDER build truck topology solids and tessellate them into MeshModels stored alongside a Solid3D placeholder entity so the GPU mesh pipeline renders them immediately. EXTRUDE picks any closed 2D profile (Circle, LwPolyline, etc.), attaches a planar face via try_attach_plane, and translational-sweeps it (tsweep Face → Solid) along Z by the given height. REVOLVE picks a profile, two axis points, and an angle (default 360°), then rotational-sweeps the wire (rsweep) around the axis. Three new CmdResult variants drive the operations: CommitSolid3D, ExtrudeEntity, RevolveEntity. Scene::layer_color() added to retrieve the active layer's RGBA color. Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 3 +- src/app/cmd_result.rs | 135 +++++++++++ src/app/commands.rs | 55 +++++ src/command/mod.rs | 15 ++ src/modules/insert/mod.rs | 1 + src/modules/insert/solid3d_cmds.rs | 376 +++++++++++++++++++++++++++++ src/scene/mod.rs | 8 + 7 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 src/modules/insert/solid3d_cmds.rs diff --git a/ROADMAP.md b/ROADMAP.md index e3fd5747..a0029ba3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -293,7 +293,8 @@ Underlay (PDF/DWF/DGN) | OBJ mesh içe aktarma | ✅ | | Solid3D tessellation (acadrust ACIS) | ✅ | | Boolean operasyonlar (UNION/SUBTRACT/INTERSECT) | ⬜ | -| EXTRUDE / REVOLVE / SWEEP / LOFT | ⬜ | +| EXTRUDE / REVOLVE | ✅ | +| SWEEP / LOFT | ⬜ | | 3D ARRAY | ✅ | | STL dışa aktarma (STLOUT) | ✅ | | STEP dışa aktarma | ⬜ | diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index ee0d5b08..004e2867 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -860,6 +860,141 @@ impl H7CAD { self.command_line.push_output(&format!("STRETCH: {count} entity(ies) stretched.")); self.refresh_properties(); } + // ── Solid3D creation (BOX / SPHERE / CYLINDER) ──────────────── + CmdResult::CommitSolid3D { mesh_fn } => { + use crate::modules::insert::solid3d_cmds::empty_solid3d; + self.push_undo_snapshot(i, "SOLID3D"); + let entity = empty_solid3d(); + let handle = self.tabs[i].scene.add_entity(entity); + if !handle.is_null() { + let name = format!("{}", handle.value()); + let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it + let _ = color; // color is captured inside mesh_fn + if let Some(mesh) = mesh_fn(name) { + self.tabs[i].scene.meshes.insert(handle, mesh); + } + self.tabs[i].dirty = true; + self.command_line.push_output("Solid created."); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } + + // ── EXTRUDE ──────────────────────────────────────────────────── + CmdResult::ExtrudeEntity { handle, height, 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::Vector3 as TruckVec3; + + let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned(); + if let Some(entity) = entity_opt { + let truck_entity = entity.to_truck_entity(&self.tabs[i].scene.document); + let result = truck_entity.and_then(|te| { + match te.object { + TruckObject::Contour(wire) => { + // Attach a planar face to the wire profile, then sweep. + let face = builder::try_attach_plane(&[wire]).ok()?; + // tsweep(Face) → Solid + let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, height as f64)); + 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, + } + } + _ => None, + } + }); + if let Some(mut mesh) = result { + self.push_undo_snapshot(i, "EXTRUDE"); + 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("EXTRUDE: solid created."); + } else { + self.command_line.push_error("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.)."); + } + } else { + self.command_line.push_error("EXTRUDE: entity not found."); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } + + // ── REVOLVE ──────────────────────────────────────────────────── + CmdResult::RevolveEntity { handle, axis_start, axis_end, angle_deg, 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 entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned(); + if let Some(entity) = entity_opt { + let truck_entity = entity.to_truck_entity(&self.tabs[i].scene.document); + let result = truck_entity.and_then(|te| { + let wire: Option = match te.object { + TruckObject::Contour(w) => Some(w), + TruckObject::Curve(e) => Some(std::iter::once(e).collect()), + _ => None, + }; + let wire = wire?; + let origin = Point3::new( + axis_start.x as f64, + axis_start.z as f64, + axis_start.y as f64, + ); + let dir = (axis_end - axis_start).normalize(); + let axis = TruckVec3::new(dir.x as f64, dir.z as f64, dir.y as f64); + let shell = builder::rsweep(&wire, origin, axis, Rad(angle_deg.to_radians() as f64)); + 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, "REVOLVE"); + 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!("REVOLVE: solid created ({:.0}°).", angle_deg)); + } else { + self.command_line.push_error("REVOLVE: could not revolve profile."); + } + } else { + self.command_line.push_error("REVOLVE: entity not found."); + } + 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 b8fa4d5a..0d08b589 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3402,6 +3402,61 @@ impl H7CAD { } } + // ── 3D Primitive — BOX ──────────────────────────────────────── + "BOX" => { + use crate::modules::insert::solid3d_cmds::BoxCommand; + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + let cmd = BoxCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + // ── 3D Primitive — SPHERE ───────────────────────────────────── + "SPHERE" => { + use crate::modules::insert::solid3d_cmds::SphereCommand; + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + let cmd = SphereCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + // ── 3D Primitive — CYLINDER ─────────────────────────────────── + "CYLINDER" => { + use crate::modules::insert::solid3d_cmds::CylinderCommand; + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + let cmd = CylinderCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + // ── EXTRUDE ──────────────────────────────────────────────────── + "EXTRUDE"|"EXT" => { + use crate::modules::insert::solid3d_cmds::ExtrudeCommand; + // If a single entity is already selected, skip the pick step. + let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + if selected.len() == 1 { + let handle = selected[0].0; + let mut cmd = ExtrudeCommand::new(color); + cmd.on_entity_pick(handle, glam::Vec3::ZERO); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let cmd = ExtrudeCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } + + // ── REVOLVE ──────────────────────────────────────────────────── + "REVOLVE"|"REV" => { + use crate::modules::insert::solid3d_cmds::RevolveCommand; + let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); + let cmd = RevolveCommand::new(color); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + // ── STL export ──────────────────────────────────────────────── "STLOUT"|"EXPORTSTL" => { return Task::done(Message::StlExport); diff --git a/src/command/mod.rs b/src/command/mod.rs index 77b953c8..4357546b 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -138,6 +138,21 @@ pub enum CmdResult { /// Translation vector to apply to vertices inside the window. delta: Vec3, }, + /// Create a Solid3D placeholder entity + associated MeshModel. + /// `mesh_fn` is called with the entity's handle string to build the mesh. + CommitSolid3D { + mesh_fn: Box Option + Send>, + }, + /// Extrude the profile entity `handle` by `height` along Z. + ExtrudeEntity { handle: Handle, height: f32, color: [f32; 4] }, + /// Revolve the profile entity `handle` around the given axis by `angle_deg`. + RevolveEntity { + handle: Handle, + axis_start: glam::Vec3, + axis_end: glam::Vec3, + angle_deg: f32, + 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/mod.rs b/src/modules/insert/mod.rs index e738f86b..1dd4190f 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod create_block; mod cylinder; pub(crate) mod insert_block; mod open_obj; +pub(crate) mod solid3d_cmds; mod sphere; pub(crate) mod wblock; pub(crate) mod xattach; diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs new file mode 100644 index 00000000..68b02084 --- /dev/null +++ b/src/modules/insert/solid3d_cmds.rs @@ -0,0 +1,376 @@ +// 3D solid primitive commands — BOX, SPHERE, CYLINDER +// and 2D→3D extrusion commands — EXTRUDE, REVOLVE. +// +// All create a minimal Solid3D entity (empty ACIS, just as a document +// placeholder to hold the handle) and a MeshModel built with truck. +// The mesh is manually inserted into scene.meshes so it renders immediately. +// +// Round-trip limitation: saving and reopening the file will not restore +// the mesh because the ACIS data is empty. Full ACIS generation requires +// a separate step (out of scope here). +// +// Coordinate convention in H7CAD: the viewport is Y-up (OpenGL style), +// so screen X→DXF X, screen Z→DXF Y, screen Y→DXF Z (height). +// truck works in standard math coordinates; we map accordingly. + +use acadrust::{entities::Solid3D, EntityType}; +use glam::Vec3; +use truck_modeling::builder; +use truck_modeling::{Point3, Rad, Vector3 as TruckVec3}; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::mesh_model::MeshModel; +use crate::scene::truck_tess; + +// ── Tessellation helper ──────────────────────────────────────────────────── + +fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) -> Option { + match truck_tess::tessellate_solid(solid) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel { + name: name.to_string(), + verts, normals, indices, + color, + selected: false, + }), + _ => None, + } +} + +fn shell_to_mesh(shell: &truck_modeling::Shell, color: [f32; 4], name: &str) -> Option { + match truck_tess::tessellate_shell(shell) { + truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel { + name: name.to_string(), + verts, normals, indices, + color, + selected: false, + }), + _ => None, + } +} + +// ── BOX command ──────────────────────────────────────────────────────────── + +pub struct BoxCommand { + step: BoxStep, + p1: Vec3, + p2: Vec3, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum BoxStep { Corner1, Corner2, Height } + +impl BoxCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { step: BoxStep::Corner1, p1: Vec3::ZERO, p2: Vec3::ZERO, color } + } +} + +impl CadCommand for BoxCommand { + fn name(&self) -> &'static str { "BOX" } + fn prompt(&self) -> String { + match self.step { + BoxStep::Corner1 => "BOX First corner:".into(), + BoxStep::Corner2 => "BOX Opposite corner (XY):".into(), + BoxStep::Height => "BOX Height:".into(), + } + } + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match self.step { + BoxStep::Corner1 => { self.p1 = pt; self.step = BoxStep::Corner2; CmdResult::NeedPoint } + BoxStep::Corner2 => { self.p2 = pt; self.step = BoxStep::Height; CmdResult::NeedPoint } + BoxStep::Height => commit_box(self.p1, self.p2, (pt.y - self.p1.y).abs().max(1e-4), self.color), + } + } + fn wants_text_input(&self) -> bool { self.step == BoxStep::Height } + fn on_text_input(&mut self, text: &str) -> Option { + text.trim().parse::().ok().filter(|&h| h.abs() > 1e-6) + .map(|h| commit_box(self.p1, self.p2, h.abs(), self.color)) + } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} + +fn commit_box(p1: Vec3, p2: Vec3, height: f32, color: [f32; 4]) -> CmdResult { + // Map H7CAD coords to truck: x→x, z→y, y→z + let x0 = p1.x.min(p2.x) as f64; + let y0 = p1.z.min(p2.z) as f64; + let x1 = p1.x.max(p2.x) as f64; + let y1 = p1.z.max(p2.z) as f64; + let z0 = p1.y as f64; + let h = height as f64; + + // Build face at z=z0, then sweep to z0+h. + let v00 = builder::vertex(Point3::new(x0, y0, z0)); + let v10 = builder::vertex(Point3::new(x1, y0, z0)); + let v11 = builder::vertex(Point3::new(x1, y1, z0)); + let v01 = builder::vertex(Point3::new(x0, y1, z0)); + let e0 = builder::line(&v00, &v10); + let e1 = builder::line(&v10, &v11); + let e2 = builder::line(&v11, &v01); + let e3 = builder::line(&v01, &v00); + let wire: truck_modeling::Wire = [e0, e1, e2, e3].into_iter().collect(); + let face = match builder::try_attach_plane(&[wire]) { + Ok(f) => f, + Err(_) => return CmdResult::Cancel, + }; + // tsweep on Face → Solid + let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h)); + CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)) } +} + +// ── SPHERE command ───────────────────────────────────────────────────────── + +pub struct SphereCommand { + step: SphereStep, + center: Vec3, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum SphereStep { Center, Radius } + +impl SphereCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { step: SphereStep::Center, center: Vec3::ZERO, color } + } +} + +impl CadCommand for SphereCommand { + fn name(&self) -> &'static str { "SPHERE" } + fn prompt(&self) -> String { + match self.step { + SphereStep::Center => "SPHERE Center:".into(), + SphereStep::Radius => "SPHERE Radius:".into(), + } + } + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match self.step { + SphereStep::Center => { self.center = pt; self.step = SphereStep::Radius; CmdResult::NeedPoint } + SphereStep::Radius => commit_sphere(self.center, (pt - self.center).length(), self.color), + } + } + fn wants_text_input(&self) -> bool { self.step == SphereStep::Radius } + fn on_text_input(&mut self, text: &str) -> Option { + text.trim().parse::().ok().filter(|&r| r > 1e-6) + .map(|r| commit_sphere(self.center, r, self.color)) + } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} + +fn commit_sphere(center: Vec3, radius: f32, color: [f32; 4]) -> CmdResult { + let cx = center.x as f64; + let cy = center.z as f64; + let cz = center.y as f64; + let r = radius as f64; + + // Build a half-circle arc wire from north pole to south pole (XZ plane). + let north = builder::vertex(Point3::new(cx, cy, cz + r)); + let south = builder::vertex(Point3::new(cx, cy, cz - r)); + let east = Point3::new(cx + r, cy, cz); + let arc = builder::circle_arc(&north, &south, east); + // rsweep the wire around the Z axis for a full revolution → Shell. + let wire: truck_modeling::Wire = std::iter::once(arc).collect(); + let axis_pt = Point3::new(cx, cy, cz); + let axis = TruckVec3::new(0.0, 0.0, 1.0); + let shell = builder::rsweep(&wire, axis_pt, axis, Rad(std::f64::consts::TAU)); + CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| shell_to_mesh(&shell, color, &name)) } +} + +// ── CYLINDER command ─────────────────────────────────────────────────────── + +pub struct CylinderCommand { + step: CylStep, + center: Vec3, + radius: f32, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum CylStep { Center, Radius, Height } + +impl CylinderCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { step: CylStep::Center, center: Vec3::ZERO, radius: 1.0, color } + } +} + +impl CadCommand for CylinderCommand { + fn name(&self) -> &'static str { "CYLINDER" } + fn prompt(&self) -> String { + match self.step { + CylStep::Center => "CYLINDER Center of base:".into(), + CylStep::Radius => "CYLINDER Radius:".into(), + CylStep::Height => "CYLINDER Height:".into(), + } + } + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match self.step { + CylStep::Center => { self.center = pt; self.step = CylStep::Radius; CmdResult::NeedPoint } + CylStep::Radius => { + self.radius = (pt - self.center).length().max(1e-4); + self.step = CylStep::Height; + CmdResult::NeedPoint + } + CylStep::Height => commit_cylinder(self.center, self.radius, (pt.y - self.center.y).abs().max(1e-4), self.color), + } + } + fn wants_text_input(&self) -> bool { matches!(self.step, CylStep::Radius | CylStep::Height) } + fn on_text_input(&mut self, text: &str) -> Option { + let v = text.trim().parse::().ok().filter(|&v| v > 1e-6)?; + match self.step { + CylStep::Radius => { self.radius = v; self.step = CylStep::Height; Some(CmdResult::NeedPoint) } + CylStep::Height => Some(commit_cylinder(self.center, self.radius, v, self.color)), + _ => None, + } + } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} + +fn commit_cylinder(center: Vec3, radius: f32, height: f32, color: [f32; 4]) -> CmdResult { + let cx = center.x as f64; + let cy = center.z as f64; + let cz = center.y as f64; + let r = radius as f64; + let h = height as f64; + + // Build circle face at z=cz, sweep upward. + let right = builder::vertex(Point3::new(cx + r, cy, cz)); + let left = builder::vertex(Point3::new(cx - r, cy, cz)); + let top_t = Point3::new(cx, cy + r, cz); + let bot_t = Point3::new(cx, cy - r, cz); + let upper = builder::circle_arc(&right, &left, top_t); + let lower = builder::circle_arc(&left, &right, bot_t); + let wire: truck_modeling::Wire = [upper, lower].into_iter().collect(); + let face = match builder::try_attach_plane(&[wire]) { + Ok(f) => f, + Err(_) => return CmdResult::Cancel, + }; + let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h)); + CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)) } +} + +// ── EXTRUDE command ──────────────────────────────────────────────────────── + +pub struct ExtrudeCommand { + step: ExtrudeStep, + pub target_handle: acadrust::Handle, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum ExtrudeStep { Pick, Height } + +impl ExtrudeCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { step: ExtrudeStep::Pick, target_handle: acadrust::Handle::NULL, color } + } +} + +impl CadCommand for ExtrudeCommand { + fn name(&self) -> &'static str { "EXTRUDE" } + fn prompt(&self) -> String { + match self.step { + ExtrudeStep::Pick => "EXTRUDE Select closed profile (Circle, LwPolyline…):".into(), + ExtrudeStep::Height => "EXTRUDE Height:".into(), + } + } + fn needs_entity_pick(&self) -> bool { self.step == ExtrudeStep::Pick } + fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + self.target_handle = handle; + self.step = ExtrudeStep::Height; + CmdResult::NeedPoint + } + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if self.step == ExtrudeStep::Height { + return CmdResult::ExtrudeEntity { handle: self.target_handle, height: pt.y.abs().max(1e-4), color: self.color }; + } + CmdResult::NeedPoint + } + fn wants_text_input(&self) -> bool { self.step == ExtrudeStep::Height } + fn on_text_input(&mut self, text: &str) -> Option { + text.trim().parse::().ok().filter(|&h| h.abs() > 1e-6) + .map(|h| CmdResult::ExtrudeEntity { handle: self.target_handle, height: h.abs(), color: self.color }) + } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} + +// ── REVOLVE command ──────────────────────────────────────────────────────── + +pub struct RevolveCommand { + step: RevolveStep, + target_handle: acadrust::Handle, + axis_start: Vec3, + axis_end: Vec3, + color: [f32; 4], +} + +#[derive(PartialEq)] +enum RevolveStep { Pick, AxisStart, AxisEnd, Angle } + +impl RevolveCommand { + pub fn new(color: [f32; 4]) -> Self { + Self { + step: RevolveStep::Pick, + target_handle: acadrust::Handle::NULL, + axis_start: Vec3::ZERO, + axis_end: Vec3::new(0.0, 0.0, 1.0), + color, + } + } +} + +impl CadCommand for RevolveCommand { + fn name(&self) -> &'static str { "REVOLVE" } + fn prompt(&self) -> String { + match self.step { + RevolveStep::Pick => "REVOLVE Select profile:".into(), + RevolveStep::AxisStart => "REVOLVE Axis start point:".into(), + RevolveStep::AxisEnd => "REVOLVE Axis end point:".into(), + RevolveStep::Angle => "REVOLVE Angle of revolution <360>:".into(), + } + } + fn needs_entity_pick(&self) -> bool { self.step == RevolveStep::Pick } + fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + self.target_handle = handle; + self.step = RevolveStep::AxisStart; + CmdResult::NeedPoint + } + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match self.step { + RevolveStep::AxisStart => { self.axis_start = pt; self.step = RevolveStep::AxisEnd; CmdResult::NeedPoint } + RevolveStep::AxisEnd => { self.axis_end = pt; self.step = RevolveStep::Angle; CmdResult::NeedPoint } + RevolveStep::Angle => self.make_revolve(360.0), + _ => CmdResult::NeedPoint, + } + } + fn wants_text_input(&self) -> bool { self.step == RevolveStep::Angle } + fn on_text_input(&mut self, text: &str) -> Option { + let angle = if text.trim().is_empty() { 360.0f32 } + else { text.trim().parse::().ok().filter(|&a| a.abs() > 1e-3)? }; + Some(self.make_revolve(angle.abs())) + } + fn on_enter(&mut self) -> CmdResult { + if self.step == RevolveStep::Angle { self.make_revolve(360.0) } else { CmdResult::Cancel } + } +} + +impl RevolveCommand { + fn make_revolve(&self, angle_deg: f32) -> CmdResult { + CmdResult::RevolveEntity { + handle: self.target_handle, + axis_start: self.axis_start, + axis_end: self.axis_end, + angle_deg, + color: self.color, + } + } +} + +// ── Placeholder Solid3D entity construction ──────────────────────────────── + +/// Create a minimal Solid3D entity with empty ACIS data (placeholder only). +pub fn empty_solid3d() -> EntityType { + EntityType::Solid3D(Solid3D::new()) +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ef1d4e84..a96d341e 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1127,6 +1127,14 @@ impl Scene { handle } + /// Returns the RGBA color for the given layer name. + pub fn layer_color(&self, layer: &str) -> [f32; 4] { + let layer_entry = self.document.layers.get(layer); + let color = layer_entry.map(|l| &l.color).unwrap_or(&acadrust::types::Color::WHITE); + let [r, g, b, _] = crate::scene::tessellate::aci_to_rgba(color); + [r, g, b, 1.0] + } + pub fn custom_block_names(&self) -> Vec { self.document .block_records