cad-editor/src/modules/insert/solid3d_cmds.rs
Hakan Seven 91ba9f7699 feat(alias): centralize command aliases in user-editable ocad.pgp (#286/#288)
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:

- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
  embedded and copied to config/ocad.pgp on first launch; only that user
  copy is read after. AA→AREA default closes #286.
- resolve_alias() rewrites the leading command token at the top of
  dispatch_command_inner, so aliases resolve for every path (families,
  plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
  surfaces its target command as the top (highlighted) suggestion so the
  highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
  button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
  CommandRegistration arrays, keeping canonical/synonym/distinct-command
  names. ocad.pgp is now the sole source: deleting or remapping an alias
  there takes effect. CUI stays on the keyboard-shortcut panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:39:22 +03:00

335 lines
10 KiB
Rust

// 2D→3D modelling commands — EXTRUDE, REVOLVE, SWEEP, LOFT.
//
// Each picks profile/path entities and emits a `CmdResult` whose handler
// builds a truck solid and inserts its MeshModel into scene.meshes under a
// minimal placeholder Solid3D entity (empty ACIS — saving will not restore
// the mesh). The standalone primitives (BOX/CYLINDER/CONE/SPHERE/WEDGE/TORUS)
// live in the Model tab (`modules::model::primitive_cmd`).
use acadrust::{entities::Solid3D, EntityType};
use glam::{DVec3, Vec3};
use crate::command::{CadCommand, CmdResult};
// BOX / SPHERE / CYLINDER (and CONE / WEDGE / TORUS) now live in the Model tab
// (`modules::model::primitive_cmd`), which builds them as truck B-reps cached
// for the Design-group boolean tools. EXTRUDE / REVOLVE / SWEEP / LOFT remain
// here as 2D→3D operations.
// ── 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: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.step = ExtrudeStep::Height;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
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: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.step = RevolveStep::AxisStart;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
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.as_dvec3(),
axis_end: self.axis_end.as_dvec3(),
angle_deg,
color: self.color,
}
}
}
// ── 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: DVec3) -> 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: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
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: DVec3) -> 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: DVec3) -> 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).
pub fn empty_solid3d() -> EntityType {
EntityType::Solid3D(Solid3D::new())
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE"] }); // ExtrudeCommand
inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand
inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); // RevolveCommand
inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand