fix(face3d): repair edge editing

This commit is contained in:
Hakan Seven committed 2026-08-28 20:14:41 +03:00
commit 56bb652521
44 files changed
+5310 -1656

No files matched your search

+15 -2
View File
@@ -1,5 +1,4 @@
// Persistent command defaults — last-used values survive across command runs
// within the same session. Uses thread_local! so no synchronisation needed.
// Last-used command values for this session.
use std::cell::Cell;
@@ -28,6 +27,10 @@ thread_local! {
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
static HELIX_BASE_RADIUS: Cell<f64> = Cell::new(1.0);
static HELIX_HEIGHT: Cell<f64> = Cell::new(1.0);
static HELIX_TURNS: Cell<f64> = Cell::new(3.0);
static HELIX_COUNTER_CLOCKWISE: Cell<bool> = Cell::new(true);
}
macro_rules! accessors {
@@ -70,6 +73,16 @@ accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION);
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
accessors!(get_helix_base_radius, set_helix_base_radius, HELIX_BASE_RADIUS);
accessors!(get_helix_height, set_helix_height, HELIX_HEIGHT);
accessors!(get_helix_turns, set_helix_turns, HELIX_TURNS);
pub fn get_helix_counter_clockwise() -> bool {
HELIX_COUNTER_CLOCKWISE.with(|value| value.get())
}
pub fn set_helix_counter_clockwise(value: bool) {
HELIX_COUNTER_CLOCKWISE.with(|current| current.set(value));
}
accessors!(
get_donut_inner_diameter,
set_donut_inner_diameter,
+61 -70
View File
@@ -1,15 +1,15 @@
use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::Tolerance;
use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
use glam::DVec3;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use crate::command::{CadCommand, CmdOption, CmdResult};
use crate::scene::model::wire_model::WireModel;
use crate::t;
const EPSILON: f64 = 1.0e-9;
fn vector(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
@@ -38,12 +38,12 @@ fn edge_is_invisible(flags: InvisibleEdgeFlags, edge: usize) -> bool {
}
}
fn append_segment(points: &mut Vec<[f64; 3]>, start: DVec3, end: DVec3) {
fn append_segment(points: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) {
if !points.is_empty() {
points.push([f64::NAN; 3]);
}
points.push(start.to_array());
points.push(end.to_array());
points.push(start);
points.push(end);
}
pub struct Face3dCommand {
@@ -171,15 +171,23 @@ impl CadCommand for Face3dCommand {
let mut preview = Vec::new();
for edge in 0..self.points.len().saturating_sub(1) {
if !self.invisible[edge] {
append_segment(&mut preview, self.points[edge], self.points[edge + 1]);
append_segment(
&mut preview,
self.points[edge].to_array(),
self.points[edge + 1].to_array(),
);
}
}
let pending_edge = self.points.len() - 1;
if !self.invisible[pending_edge] {
append_segment(&mut preview, *self.points.last().unwrap(), cursor);
append_segment(
&mut preview,
self.points.last()?.to_array(),
cursor.to_array(),
);
}
if self.points.len() == 3 && !(self.invisible[3] || self.pending_invisible) {
append_segment(&mut preview, cursor, self.points[0]);
append_segment(&mut preview, cursor.to_array(), self.points[0].to_array());
}
Some(WireModel::solid_f64(
"face3d_preview".to_string(),
@@ -202,6 +210,7 @@ pub struct FaceEdgeCommand {
mode: EdgeMode,
show_all_hidden: bool,
displayed_faces: HashSet<Handle>,
pending_replacements: HashMap<Handle, Face3D>,
}
impl FaceEdgeCommand {
@@ -211,33 +220,32 @@ impl FaceEdgeCommand {
mode: EdgeMode::Toggle,
show_all_hidden: false,
displayed_faces: HashSet::new(),
pending_replacements: HashMap::new(),
}
}
fn corners(face: &Face3D) -> [DVec3; 4] {
fn corners(face: &Face3D) -> [KernelVec3; 4] {
[
point(face.first_corner),
point(face.second_corner),
point(face.third_corner),
point(face.fourth_corner),
KernelVec3::from(point(face.first_corner).to_array()),
KernelVec3::from(point(face.second_corner).to_array()),
KernelVec3::from(point(face.third_corner).to_array()),
KernelVec3::from(point(face.fourth_corner).to_array()),
]
}
fn nearest_edge(face: &Face3D, picked: DVec3) -> Option<usize> {
fn nearest_edge(face: &Face3D, picked: KernelVec3) -> Option<usize> {
let corners = Self::corners(face);
let tolerance = Tolerance::default().linear();
let mut best = None;
for edge in 0..4 {
let start = corners[edge];
let end = corners[(edge + 1) % 4];
let segment = end - start;
let length_squared = segment.length_squared();
if length_squared <= EPSILON * EPSILON {
if start.distance(end) <= tolerance {
continue;
}
let parameter = ((picked - start).dot(segment) / length_squared).clamp(0.0, 1.0);
let distance_squared = (picked - (start + segment * parameter)).length_squared();
if best.is_none_or(|(_, distance)| distance_squared < distance) {
best = Some((edge, distance_squared));
let distance = picked.distance_to_segment(start, end);
if best.is_none_or(|(_, best_distance)| distance < best_distance) {
best = Some((edge, distance));
}
}
best.map(|(edge, _)| edge)
@@ -253,39 +261,6 @@ impl FaceEdgeCommand {
)
}
fn edges_overlap_collinearly(
selected_start: DVec3,
selected_end: DVec3,
candidate_start: DVec3,
candidate_end: DVec3,
) -> bool {
let selected = selected_end - selected_start;
let selected_length = selected.length();
let candidate = candidate_end - candidate_start;
if selected_length <= EPSILON || candidate.length() <= EPSILON {
return false;
}
let direction = selected / selected_length;
let scale = selected_length
.max(candidate.length())
.max(1.0);
let tolerance = EPSILON * scale;
let line_distance = |point: DVec3| {
let offset = point - selected_start;
(offset - direction * offset.dot(direction)).length()
};
if line_distance(candidate_start) > tolerance
|| line_distance(candidate_end) > tolerance
{
return false;
}
let t0 = (candidate_start - selected_start).dot(direction);
let t1 = (candidate_end - selected_start).dot(direction);
let candidate_min = t0.min(t1);
let candidate_max = t0.max(t1);
candidate_max.min(selected_length) - candidate_min.max(0.0) > tolerance
}
fn collinear_replacements(
&mut self,
selected_face: usize,
@@ -299,16 +274,18 @@ impl FaceEdgeCommand {
selected_edge,
);
let mut replacements = Vec::new();
for (handle, face) in &mut self.faces {
self.pending_replacements.clear();
for (handle, face) in &self.faces {
let corners = Self::corners(face);
let mut updated = face.clone();
let mut changed = false;
for edge in 0..4 {
if Self::edges_overlap_collinearly(
selected_start,
selected_end,
corners[edge],
corners[(edge + 1) % 4],
if segments_overlap_collinearly(
selected_start.to_array(),
selected_end.to_array(),
corners[edge].to_array(),
corners[(edge + 1) % 4].to_array(),
Tolerance::default().linear(),
) && edge_is_invisible(updated.invisible_edges, edge) != make_invisible
{
set_edge_invisible(&mut updated.invisible_edges, edge, make_invisible);
@@ -316,7 +293,7 @@ impl FaceEdgeCommand {
}
}
if changed {
*face = updated.clone();
self.pending_replacements.insert(*handle, updated.clone());
replacements.push((*handle, vec![EntityType::Face3D(updated)]));
}
}
@@ -332,10 +309,14 @@ impl FaceEdgeCommand {
let corners = Self::corners(face);
for edge in 0..4 {
if edge_is_invisible(face.invisible_edges, edge)
&& (corners[(edge + 1) % 4] - corners[edge]).length_squared()
> EPSILON * EPSILON
&& corners[edge].distance(corners[(edge + 1) % 4])
> Tolerance::default().linear()
{
append_segment(&mut points, corners[edge], corners[(edge + 1) % 4]);
append_segment(
&mut points,
corners[edge].to_array(),
corners[(edge + 1) % 4].to_array(),
);
}
}
}
@@ -402,6 +383,7 @@ impl CadCommand for FaceEdgeCommand {
self.displayed_faces.insert(handle);
return CmdResult::Preview(self.hidden_edges_wire());
}
let picked = KernelVec3::from(picked.to_array());
let Some(edge) = Self::nearest_edge(&self.faces[index].1, picked) else {
return CmdResult::NeedPoint;
};
@@ -424,10 +406,14 @@ impl CadCommand for FaceEdgeCommand {
let mut points = Vec::new();
let corners = Self::corners(face);
for edge in 0..4 {
if (corners[(edge + 1) % 4] - corners[edge]).length_squared()
> EPSILON * EPSILON
if corners[edge].distance(corners[(edge + 1) % 4])
> Tolerance::default().linear()
{
append_segment(&mut points, corners[edge], corners[(edge + 1) % 4]);
append_segment(
&mut points,
corners[edge].to_array(),
corners[(edge + 1) % 4].to_array(),
);
}
}
return vec![WireModel::solid_f64(
@@ -437,7 +423,7 @@ impl CadCommand for FaceEdgeCommand {
false,
)];
}
Self::nearest_edge(face, picked)
Self::nearest_edge(face, KernelVec3::from(picked.to_array()))
.map(|edge| vec![Self::edge_wire(face, edge, "face3d_edge")])
.unwrap_or_default()
}
@@ -494,7 +480,12 @@ impl CadCommand for FaceEdgeCommand {
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[Handle]) {
if let Some(new_handle) = new_handles.first().copied() {
if let Some((handle, _)) = self.faces.iter_mut().find(|(handle, _)| *handle == old) {
if let Some((handle, face)) =
self.faces.iter_mut().find(|(handle, _)| *handle == old)
{
if let Some(updated) = self.pending_replacements.remove(&old) {
*face = updated;
}
*handle = new_handle;
}
if self.displayed_faces.remove(&old) {
+368 -185
View File
@@ -1,35 +1,18 @@
// Parametric helix tool — interactive command.
//
// Command: HELIX — build a parametric 3D helix and commit it as a single 3D
// polyline whose straight segments approximate the curve. The user picks a
// base centre point, then types the base radius, the top radius (Enter keeps
// it equal to the base radius for a cylindrical helix), the height, and the
// number of turns (Enter accepts a default of three turns).
//
// The curve is sampled at a fixed angular resolution: with `SEG_PER_TURN`
// samples per turn the helix has `turns * SEG_PER_TURN` segments. For each
// sample the radius interpolates linearly from base to top, the elevation
// interpolates linearly from zero to the height, and the planar position is
// `(radius*cos(angle), radius*sin(angle))` offset from the centre.
use acadrust::entities::Polyline3D;
use acadrust::entities::{Helix, HelixConstraint, Spline};
use acadrust::types::Vector3;
use acadrust::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use cadkernel::space::{HelixCurve, HelixDirection, Vec3};
use glam::DVec3;
/// Angular samples per full turn of the helix.
const SEG_PER_TURN: usize = 36;
/// Default number of turns when the user accepts the prompt with Enter.
const DEFAULT_TURNS: f64 = 3.0;
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
// ── Ribbon definition ─────────────────────────────────────────────────────
const EPSILON: f64 = 1.0e-9;
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "HELIX",
@@ -39,16 +22,18 @@ pub fn tool() -> ToolDef {
}
}
// ── Command implementation ────────────────────────────────────────────────
/// Which parameter the command is currently collecting.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Step {
Center,
BaseRadius,
BaseDiameter,
TopRadius,
Height,
TopDiameter,
Final,
AxisEndpoint,
Turns,
TurnHeight,
Twist,
}
pub struct HelixCommand {
@@ -58,58 +43,130 @@ pub struct HelixCommand {
top_radius: f64,
height: f64,
turns: f64,
requested_turn_height: Option<f64>,
counter_clockwise: bool,
constraint: HelixConstraint,
plane: WorkingPlane,
}
impl HelixCommand {
pub fn new() -> Self {
let base_radius = defaults::get_helix_base_radius();
Self {
step: Step::Center,
center: DVec3::ZERO,
base_radius: 0.0,
top_radius: 0.0,
height: 0.0,
turns: DEFAULT_TURNS,
base_radius,
top_radius: base_radius,
height: defaults::get_helix_height(),
turns: defaults::get_helix_turns(),
requested_turn_height: None,
counter_clockwise: defaults::get_helix_counter_clockwise(),
constraint: HelixConstraint::Turns,
plane: WorkingPlane::default(),
}
}
/// Sample the helix into a list of world points.
fn sample_points(&self, base_radius: f64, top_radius: f64, height: f64, turns: f64) -> Vec<DVec3> {
let turns = if turns <= 0.0 { DEFAULT_TURNS } else { turns };
let segments = ((turns * SEG_PER_TURN as f64).round() as usize).max(1);
let mut pts = Vec::with_capacity(segments + 1);
for t in 0..=segments {
let frac = t as f64 / segments as f64;
let angle = turns * std::f64::consts::TAU * frac;
let radius = base_radius + (top_radius - base_radius) * frac;
let z = height * frac;
pts.push(
self.center
+ self.plane.x * (radius * angle.cos())
+ self.plane.y * (radius * angle.sin())
+ self.plane.z * z,
);
}
pts
}
/// Build the committed helix entity from the collected parameters.
fn build(&self) -> Option<EntityType> {
let pts = self.sample_points(self.base_radius, self.top_radius, self.height, self.turns);
if pts.len() < 2 {
return None;
}
let verts: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
let mut pl = Polyline3D::from_points(verts);
pl.flags.closed = false;
Some(EntityType::Polyline3D(pl))
}
/// Parse a positive distance from typed text; returns `None` on failure.
fn parse_value(text: &str) -> Option<f64> {
crate::entities::common::parse_typed_length(text)
}
fn parse_turns(text: &str) -> Option<f64> {
text.trim().replace(',', ".").parse().ok().filter(|value: &f64| value.is_finite())
}
fn axis_start_direction(&self, axis: Vec3) -> Option<Vec3> {
let project = |direction: DVec3| {
let direction = Vec3::from(direction.to_array());
(direction - axis * direction.dot(axis)).normalize()
};
project(self.plane.x).or_else(|| project(self.plane.y))
}
fn resolved_turns(&self, height: f64) -> f64 {
match self.requested_turn_height {
Some(turn_height) if turn_height > EPSILON => (height.abs() / turn_height).max(EPSILON),
_ => self.turns,
}
}
fn kernel_curve(&self, height: f64, axis: DVec3) -> Option<HelixCurve> {
let axis = Vec3::from(axis.to_array())
.normalize()
.or_else(|| Vec3::from(self.plane.z.to_array()).normalize())?;
Some(HelixCurve {
base_center: self.center.to_array(),
axis_direction: axis.to_array(),
start_direction: self.axis_start_direction(axis)?.to_array(),
base_radius: self.base_radius,
top_radius: self.top_radius,
height,
turns: self.resolved_turns(height),
direction: if self.counter_clockwise {
HelixDirection::CounterClockwise
} else {
HelixDirection::Clockwise
},
})
}
fn build(&self, height: f64, axis: DVec3) -> Option<EntityType> {
let curve = self.kernel_curve(height, axis)?;
let nurbs = curve.nurbs()?;
let axis = DVec3::from_array(curve.axis_direction).normalize_or(self.plane.z);
let start_direction = DVec3::from_array(curve.start_direction).normalize_or(self.plane.x);
let mut spline = Spline::new();
spline.degree = nurbs.degree() as i32;
spline.knots = nurbs.knots().to_vec();
spline.control_points = nurbs
.control_points()
.iter()
.map(|point| Vector3::new(point[0], point[1], point[2]))
.collect();
spline.flags.planar = false;
spline.flags.rational = nurbs.is_rational();
if spline.flags.rational {
spline.weights = nurbs.weights().to_vec();
}
let mut helix = Helix::new();
helix.spline = spline;
helix.axis_base_point = Vector3::new(self.center.x, self.center.y, self.center.z);
let start = self.center + start_direction * curve.base_radius;
helix.start_point = Vector3::new(start.x, start.y, start.z);
helix.axis_vector = Vector3::new(axis.x, axis.y, axis.z);
helix.radius = curve.base_radius;
helix.turns = curve.turns;
helix.turn_height = height / curve.turns;
helix.handedness = self.counter_clockwise;
helix.constraint = self.constraint;
Some(EntityType::Helix(helix))
}
fn commit(&mut self, height: f64, axis: DVec3) -> CmdResult {
let Some(entity) = self.build(height, axis) else {
return CmdResult::NeedPoint;
};
self.height = height;
self.turns = self.resolved_turns(height);
defaults::set_helix_base_radius(self.base_radius);
defaults::set_helix_height(height);
defaults::set_helix_turns(self.turns);
defaults::set_helix_counter_clockwise(self.counter_clockwise);
CmdResult::CommitAndExit(entity)
}
fn preview(&self, height: f64, axis: DVec3) -> Option<WireModel> {
let points = self
.kernel_curve(height, axis)?
.nurbs()?
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
Some(WireModel::solid_f64(
"helix_preview".to_string(),
points,
WireModel::CYAN,
false,
))
}
}
impl CadCommand for HelixCommand {
@@ -123,65 +180,158 @@ impl CadCommand for HelixCommand {
fn prompt(&self) -> String {
match self.step {
Step::Center => t!("HELIX Specify centre point of base:").into_owned(),
Step::BaseRadius => t!("HELIX Specify base radius:").into_owned(),
Step::TopRadius => t!("HELIX Specify top radius <same as base>:").into_owned(),
Step::Height => t!("HELIX Specify helix height:").into_owned(),
Step::Turns => t!("HELIX Enter number of turns <3>:").into_owned(),
Step::Center => crate::tf!(
"HELIX Number of turns = {:.4} Twist = {}\nSpecify center point of base:",
self.turns,
if self.counter_clockwise { "CCW" } else { "CW" }
)
.into_owned(),
Step::BaseRadius => crate::tf!(
"HELIX Specify base radius or [Diameter] <{:.4}>:",
self.base_radius
)
.into_owned(),
Step::BaseDiameter => crate::tf!(
"HELIX Specify base diameter <{:.4}>:",
self.base_radius * 2.0
)
.into_owned(),
Step::TopRadius => crate::tf!(
"HELIX Specify top radius or [Diameter] <{:.4}>:",
self.base_radius
)
.into_owned(),
Step::TopDiameter => crate::tf!(
"HELIX Specify top diameter <{:.4}>:",
self.base_radius * 2.0
)
.into_owned(),
Step::Final => crate::tf!(
"HELIX Specify helix height or [Axis endpoint/Turns/turn Height/tWist] <{:.4}>:",
self.height
)
.into_owned(),
Step::AxisEndpoint => t!("HELIX Specify axis endpoint:").into_owned(),
Step::Turns => crate::tf!("HELIX Enter number of turns <{:.4}>:", self.turns).into_owned(),
Step::TurnHeight => crate::tf!(
"HELIX Specify distance between turns <{:.4}>:",
self.height.abs() / self.turns.max(EPSILON)
)
.into_owned(),
Step::Twist => crate::tf!(
"HELIX Enter twist direction [Clockwise/Counterclockwise] <{}>:",
if self.counter_clockwise { "CCW" } else { "CW" }
)
.into_owned(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::BaseRadius | Step::TopRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
Step::Final => vec![
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
CmdOption::new(t!("Turns").as_ref(), "T"),
CmdOption::new(t!("Turn Height").as_ref(), "H"),
CmdOption::new(t!("Twist").as_ref(), "W"),
],
Step::Twist => vec![
CmdOption::new(t!("Clockwise").as_ref(), "CW"),
CmdOption::new(t!("Counterclockwise").as_ref(), "CCW"),
],
_ => Vec::new(),
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::BaseRadius | Step::TopRadius | Step::Final | Step::Twist)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Center => {
self.center = pt;
self.center = point;
self.step = Step::BaseRadius;
CmdResult::NeedPoint
}
Step::BaseRadius => {
// A point click resolves the base radius as the planar distance
// from the centre to the picked point.
let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r > 0.0 {
self.base_radius = r;
Step::BaseRadius | Step::BaseDiameter => {
let local = self.plane.vector_to_local(point - self.center);
let distance = local.x.hypot(local.y);
let radius = if self.step == Step::BaseDiameter {
distance * 0.5
} else {
distance
};
if radius > EPSILON {
self.base_radius = radius;
self.top_radius = radius;
self.step = Step::TopRadius;
}
CmdResult::NeedPoint
}
Step::TopRadius => {
let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
self.top_radius = if r > 0.0 { r } else { self.base_radius };
self.step = Step::Height;
Step::TopRadius | Step::TopDiameter => {
let local = self.plane.vector_to_local(point - self.center);
let distance = local.x.hypot(local.y);
self.top_radius = if self.step == Step::TopDiameter {
distance * 0.5
} else {
distance
};
self.step = Step::Final;
CmdResult::NeedPoint
}
Step::Height => {
// Use the elevation of the picked point relative to the centre.
let h = (pt - self.center).dot(self.plane.z);
if h.abs() > 0.0 {
self.height = h;
self.step = Step::Turns;
Step::Final => {
let height = (point - self.center).dot(self.plane.z);
if height.abs() <= EPSILON {
CmdResult::NeedPoint
} else {
self.commit(height, self.plane.z)
}
CmdResult::NeedPoint
}
Step::Turns => CmdResult::NeedPoint,
Step::AxisEndpoint => {
let vector = point - self.center;
let height = vector.length();
if height <= EPSILON {
CmdResult::NeedPoint
} else {
self.commit(height, vector / height)
}
}
Step::Turns | Step::TurnHeight | Step::Twist => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::TopRadius => {
// Enter keeps the top radius equal to the base radius.
Step::Center => CmdResult::Cancel,
Step::BaseRadius | Step::BaseDiameter => {
self.top_radius = self.base_radius;
self.step = Step::Height;
self.step = Step::TopRadius;
CmdResult::NeedPoint
}
Step::TopRadius | Step::TopDiameter => {
self.top_radius = self.base_radius;
self.step = Step::Final;
CmdResult::NeedPoint
}
Step::Final => self.commit(self.height, self.plane.z),
Step::AxisEndpoint => CmdResult::NeedPoint,
Step::Turns => {
self.constraint = HelixConstraint::Turns;
self.requested_turn_height = None;
self.step = Step::Final;
CmdResult::NeedPoint
}
Step::TurnHeight => {
self.constraint = HelixConstraint::TurnHeight;
self.requested_turn_height = Some(self.height.abs() / self.turns.max(EPSILON));
self.step = Step::Final;
CmdResult::NeedPoint
}
Step::Twist => {
self.step = Step::Final;
CmdResult::NeedPoint
}
Step::Turns => match self.build() {
Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel,
},
_ => CmdResult::NeedPoint,
}
}
@@ -190,123 +340,156 @@ impl CadCommand for HelixCommand {
}
fn wants_text_input(&self) -> bool {
// Every step past the centre pick reads a typed numeric value.
self.step != Step::Center
}
fn dyn_field(&self) -> DynField {
match self.step {
Step::Center => DynField::Point,
Step::BaseRadius | Step::TopRadius => DynField::Distance,
Step::Height => DynField::Distance,
Step::Turns => DynField::Scalar,
Step::Center | Step::AxisEndpoint => DynField::Point,
Step::BaseRadius | Step::BaseDiameter | Step::TopRadius | Step::TopDiameter | Step::Final | Step::TurnHeight => DynField::Distance,
Step::Turns | Step::Twist => DynField::Scalar,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
let value = text.trim();
let keyword = value.to_ascii_uppercase();
match self.step {
Step::Center => None,
Step::BaseRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
self.step = Step::BaseDiameter;
Some(CmdResult::NeedPoint)
}
Step::TopRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
self.step = Step::TopDiameter;
Some(CmdResult::NeedPoint)
}
Step::Final if matches!(keyword.as_str(), "A" | "AXIS" | "AXIS ENDPOINT") => {
self.step = Step::AxisEndpoint;
Some(CmdResult::NeedPoint)
}
Step::Final if matches!(keyword.as_str(), "T" | "TURNS") => {
self.step = Step::Turns;
Some(CmdResult::NeedPoint)
}
Step::Final if matches!(keyword.as_str(), "H" | "HEIGHT" | "TURN HEIGHT") => {
self.step = Step::TurnHeight;
Some(CmdResult::NeedPoint)
}
Step::Final if matches!(keyword.as_str(), "W" | "TWIST") => {
self.step = Step::Twist;
Some(CmdResult::NeedPoint)
}
Step::BaseRadius => {
if let Some(v) = Self::parse_value(t) {
if v > 0.0 {
self.base_radius = v;
self.step = Step::TopRadius;
}
let radius = Self::parse_value(value)?;
if radius > EPSILON {
self.base_radius = radius;
self.top_radius = radius;
self.step = Step::TopRadius;
}
Some(CmdResult::NeedPoint)
}
Step::BaseDiameter => {
let diameter = Self::parse_value(value)?;
if diameter > EPSILON {
self.base_radius = diameter * 0.5;
self.top_radius = self.base_radius;
self.step = Step::TopRadius;
}
Some(CmdResult::NeedPoint)
}
Step::TopRadius => {
if t.is_empty() {
// Empty entry keeps the top radius equal to the base radius.
self.top_radius = self.base_radius;
self.step = Step::Height;
} else if let Some(v) = Self::parse_value(t) {
self.top_radius = if v > 0.0 { v } else { self.base_radius };
self.step = Step::Height;
let radius = Self::parse_value(value)?;
if radius >= 0.0 {
self.top_radius = radius;
self.step = Step::Final;
}
Some(CmdResult::NeedPoint)
}
Step::Height => {
if let Some(v) = Self::parse_value(t) {
if v != 0.0 {
self.height = v;
self.step = Step::Turns;
}
Step::TopDiameter => {
let diameter = Self::parse_value(value)?;
if diameter >= 0.0 {
self.top_radius = diameter * 0.5;
self.step = Step::Final;
}
Some(CmdResult::NeedPoint)
}
Step::Final => {
let height = Self::parse_value(value)?;
(height.abs() > EPSILON).then(|| self.commit(height, self.plane.z))
}
Step::AxisEndpoint => None,
Step::Turns => {
self.turns = if t.is_empty() {
DEFAULT_TURNS
} else {
match Self::parse_value(t) {
Some(v) if v > 0.0 => v,
_ => return Some(CmdResult::NeedPoint),
}
};
match self.build() {
Some(e) => Some(CmdResult::CommitAndExit(e)),
None => Some(CmdResult::Cancel),
let turns = Self::parse_turns(value)?;
if turns > EPSILON {
self.turns = turns;
self.requested_turn_height = None;
self.constraint = HelixConstraint::Turns;
self.step = Step::Final;
}
Some(CmdResult::NeedPoint)
}
Step::TurnHeight => {
let turn_height = Self::parse_value(value)?;
if turn_height > EPSILON {
self.requested_turn_height = Some(turn_height);
self.constraint = HelixConstraint::TurnHeight;
self.step = Step::Final;
}
Some(CmdResult::NeedPoint)
}
Step::Twist => {
match keyword.as_str() {
"C" | "CW" | "CLOCKWISE" => self.counter_clockwise = false,
"CC" | "CCW" | "COUNTERCLOCKWISE" => self.counter_clockwise = true,
_ => return Some(CmdResult::NeedPoint),
}
self.step = Step::Final;
Some(CmdResult::NeedPoint)
}
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
// Preview the helix once enough parameters are known. Before a base
// radius is fixed, track the cursor distance to size a live ring.
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
match self.step {
Step::Center => None,
Step::BaseRadius => {
let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r <= 0.0 {
return None;
}
let pts = self.sample_points(r, r, 0.0, 1.0);
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
Some(WireModel::solid_f64(
"helix_preview".to_string(),
raw,
WireModel::CYAN,
false,
))
}
_ => {
// Use the parameters fixed so far; for the height step let the
// cursor elevation drive the live height.
let height = if self.step == Step::Height {
(pt - self.center).dot(self.plane.z)
Step::BaseRadius | Step::BaseDiameter => {
let local = self.plane.vector_to_local(point - self.center);
let old = (self.base_radius, self.top_radius);
let distance = local.x.hypot(local.y);
self.base_radius = if self.step == Step::BaseDiameter {
distance * 0.5
} else {
self.height
};
let top = if self.step == Step::TopRadius {
let delta = self.plane.vector_to_local(pt - self.center);
let r = delta.x.hypot(delta.y);
if r > 0.0 {
r
} else {
self.base_radius
}
} else {
self.top_radius
};
let pts = self.sample_points(self.base_radius, top, height, self.turns);
if pts.len() < 2 {
return None;
distance
}
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
Some(WireModel::solid_f64(
"helix_preview".to_string(),
raw,
WireModel::CYAN,
false,
))
.max(EPSILON);
self.top_radius = self.base_radius;
let preview = self.preview(0.0, self.plane.z);
(self.base_radius, self.top_radius) = old;
preview
}
Step::TopRadius | Step::TopDiameter => {
let local = self.plane.vector_to_local(point - self.center);
let old = self.top_radius;
let distance = local.x.hypot(local.y);
self.top_radius = if self.step == Step::TopDiameter {
distance * 0.5
} else {
distance
};
let preview = self.preview(0.0, self.plane.z);
self.top_radius = old;
preview
}
Step::Final => self.preview((point - self.center).dot(self.plane.z), self.plane.z),
Step::AxisEndpoint => {
let vector = point - self.center;
let height = vector.length();
(height > EPSILON).then(|| self.preview(height, vector / height)).flatten()
}
Step::Turns | Step::TurnHeight | Step::Twist => self.preview(self.height, self.plane.z),
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] }); // HelixCommand
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] });
+6 -13
View File
@@ -1,11 +1,4 @@
// 3D polyline tool — interactive command.
//
// Command: 3DPOLY — pick a sequence of points whose Z ordinates may differ and
// commit them as a single 3D polyline of straight segments. Unlike PLINE (a
// planar lightweight polyline with bulge arcs), every vertex keeps its own Z,
// so the result is a true non-planar path. Close (C) adds a closing segment
// back to the first point; Undo (U) drops the last picked vertex. Enter / Esc
// finishes the open polyline.
// 3DPOLY creates open or closed non-planar paths.
use acadrust::entities::Polyline3D;
use acadrust::types::Vector3;
@@ -67,7 +60,7 @@ impl CadCommand for Poly3dCommand {
fn prompt(&self) -> String {
match self.points.len() {
0 => t!("3DPOLY Specify start point of polyline:").into_owned(),
1 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
1 | 2 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
_ => t!("3DPOLY Specify next point [Close/Undo]:").into_owned(),
}
}
@@ -98,10 +91,10 @@ impl CadCommand for Poly3dCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match text.trim().to_uppercase().as_str() {
"C" | "CLOSE" => match self.build(true) {
Some(e) => Some(CmdResult::CommitAndExit(e)),
None => Some(CmdResult::NeedPoint),
},
"C" | "CLOSE" if self.points.len() >= 3 => self
.build(true)
.map(CmdResult::CommitAndExit),
"C" | "CLOSE" => Some(CmdResult::NeedPoint),
"U" | "UNDO" => {
self.points.pop();
Some(CmdResult::NeedPoint)