Files
OneCAD-Draft/src/modules/insert/solid3d_cmds.rs
T
Hakan SevenandClaude Opus 4.8 57710eec2b fix(precision): keep typed coordinates exact in f64 (#311)
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 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00

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