forked from 1b-agent/opencadstudio-upstream
Typed values were quantized to the f32 grid before reaching the entity (e.g. a length of 2000.8 committed as 2000.8020), worsening with magnitude. The interaction point layer was all glam::Vec3 (f32). Two-layer fix: 1. Coordinate backbone -> f64. last_point, last_cursor_world, dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord, the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance paths, command-line coordinate entry and the coordinate readout all compute in f64. Screen/GPU/alignment stays f32 (snap engine, render geometry, projection, UCS-icon/viewcube drawing) with casts only at those boundaries. 2. Per-command committed storage -> DVec3. on_point already received a DVec3, but many command impls narrowed it back to f32 (let pt = pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN, INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud, wipeout, attdef, table, tolerance, array center, mview, plot_window, plus the QDIM_PLACE handler and qdim_collect_points. Preview, hit-test and rubber-band paths keep f32 at the boundary. Hatch manual boundaries are left for a follow-up: HatchModel.boundary is structurally Arc<Vec<[f32;2]>> and needs a model-level widen. Co-Authored-By: Claude Opus 4.8 <[email protected]>
336 lines
10 KiB
Rust
336 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;
|
|
|
|
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: DVec3,
|
|
axis_end: DVec3,
|
|
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: DVec3::ZERO,
|
|
axis_end: DVec3::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 {
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 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
|