feat: BOX, SPHERE, CYLINDER primitives + EXTRUDE / REVOLVE commands
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 <noreply@anthropic.com>
This commit is contained in:
parent
a773a1a1f1
commit
549ae38459
7 changed files with 592 additions and 1 deletions
|
|
@ -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 | ⬜ |
|
||||
|
|
|
|||
|
|
@ -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<truck_modeling::Wire> = 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<dyn FnOnce(String) -> Option<crate::scene::mesh_model::MeshModel> + 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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
376
src/modules/insert/solid3d_cmds.rs
Normal file
376
src/modules/insert/solid3d_cmds.rs
Normal file
|
|
@ -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<MeshModel> {
|
||||
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<MeshModel> {
|
||||
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<CmdResult> {
|
||||
text.trim().parse::<f32>().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<CmdResult> {
|
||||
text.trim().parse::<f32>().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<CmdResult> {
|
||||
let v = text.trim().parse::<f32>().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<CmdResult> {
|
||||
text.trim().parse::<f32>().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<CmdResult> {
|
||||
let angle = if text.trim().is_empty() { 360.0f32 }
|
||||
else { text.trim().parse::<f32>().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())
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
self.document
|
||||
.block_records
|
||||
|
|
|
|||
Loading…
Reference in a new issue