forked from 1b-agent/opencadstudio-upstream
Merge pull request #907 from ramox81/codex/fix-helix-command-properties
Fix parametric helix creation and properties
This commit is contained in:
5 files changed
+793
-274
No files matched your search
Generated
+2
-2
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.4.1"
|
||||
source = "git+https://[email protected]/HakanSeven12/cadcodec.git?rev=788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed#788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed"
|
||||
source = "git+https://[email protected]/HakanSeven12/cadcodec.git?rev=9a332bb3c48eca90c3eefaa17aed514da0b8694a#9a332bb3c48eca90c3eefaa17aed514da0b8694a"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b2b1d4b#b2b1d4b0cd7def2d26a0d519991fc89a89f916d3"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1283cce45439abecdfd49828c7d6b84cb8ba0181#1283cce45439abecdfd49828c7d6b84cb8ba0181"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+3
-3
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
||||
rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1283cce45439abecdfd49828c7d6b84cb8ba0181", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
@@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa
|
||||
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
|
||||
[patch."https://github.com/HakanSeven12/cadcodec.git"]
|
||||
acadrust = { git = "https://[email protected]/HakanSeven12/cadcodec.git", rev = "788eea0161ba9f3eb7ee6569fd8bb52a8207a2ed" }
|
||||
acadrust = { git = "https://[email protected]/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
|
||||
|
||||
+405
-82
@@ -1,106 +1,427 @@
|
||||
// Helix entity (AcDbHelix): a spline curve plus its generating parameters.
|
||||
//
|
||||
// A loaded helix carries a fully evaluated `spline` (the NURBS the file
|
||||
// stored) alongside the parameters that generated it (base point, axis,
|
||||
// radius, turns, turn height, twist, constraint). We render and grip the
|
||||
// entity through its embedded spline, and expose the generating parameters
|
||||
// as the Geometry property group.
|
||||
|
||||
use acadrust::entities::{Helix, HelixConstraint};
|
||||
use crate::t;
|
||||
use acadrust::entities::{Helix, HelixConstraint, Spline};
|
||||
use acadrust::types::Vector3;
|
||||
use cadkernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::ro_prop as ro;
|
||||
use crate::entities::traits::{Grippable, Transformable, RenderConvertible};
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_prop as edit, format_angle, format_length, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, RenderConvertible, Transformable};
|
||||
use crate::scene::convert::acad_to_render::RenderEntity;
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::t;
|
||||
|
||||
/// Perpendicular distance from `p` to the helix axis (line through
|
||||
/// `axis_base_point` along `axis_vector`). Used to recover the top radius
|
||||
/// from the last defining point of the spline.
|
||||
fn radius_from_axis(helix: &Helix, p: &acadrust::types::Vector3) -> Option<f64> {
|
||||
let base = &helix.axis_base_point;
|
||||
let axis = helix.axis_vector.normalize();
|
||||
// A zero axis vector cannot define a line.
|
||||
if axis.length() < 1e-9 {
|
||||
return None;
|
||||
const EPSILON: f64 = 1.0e-9;
|
||||
|
||||
fn point(vector: Vector3) -> Vec3 {
|
||||
Vec3::new(vector.x, vector.y, vector.z)
|
||||
}
|
||||
|
||||
fn vector(value: Vec3) -> Vector3 {
|
||||
Vector3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn display(value: Vec3) -> DVec3 {
|
||||
DVec3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn kernel(value: DVec3) -> Vec3 {
|
||||
Vec3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn finite(value: Vec3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
fn spline_curve(spline: &Spline) -> Option<NurbsCurve3> {
|
||||
let controls: Vec<[f64; 3]> = spline
|
||||
.control_points
|
||||
.iter()
|
||||
.map(|value| [value.x, value.y, value.z])
|
||||
.collect();
|
||||
let degree = usize::try_from(spline.degree).ok()?;
|
||||
let weights = if spline.weights.is_empty() {
|
||||
vec![1.0; controls.len()]
|
||||
} else {
|
||||
spline.weights.clone()
|
||||
};
|
||||
NurbsCurve3::new_strict(degree, controls, spline.knots.clone(), weights)
|
||||
}
|
||||
|
||||
fn axis_frame(helix: &Helix) -> Option<(Vec3, Vec3, Vec3)> {
|
||||
let base = point(helix.axis_base_point);
|
||||
let axis = point(helix.axis_vector).normalize()?;
|
||||
let radial = point(helix.start_point) - base;
|
||||
let start_direction = (radial - axis * radial.dot(axis)).normalize()?;
|
||||
Some((base, axis, start_direction))
|
||||
}
|
||||
|
||||
fn radius_from_axis(helix: &Helix, value: Vec3) -> Option<f64> {
|
||||
let (base, axis, _) = axis_frame(helix)?;
|
||||
let delta = value - base;
|
||||
Some((delta - axis * delta.dot(axis)).length())
|
||||
}
|
||||
|
||||
fn top_radius(helix: &Helix) -> f64 {
|
||||
spline_curve(&helix.spline)
|
||||
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||
.and_then(|endpoint| radius_from_axis(helix, endpoint))
|
||||
.unwrap_or(helix.radius)
|
||||
}
|
||||
|
||||
fn kernel_curve(helix: &Helix, top_radius: f64) -> Option<HelixCurve> {
|
||||
let (base, axis, start_direction) = axis_frame(helix)?;
|
||||
Some(HelixCurve {
|
||||
base_center: base.to_array(),
|
||||
axis_direction: axis.to_array(),
|
||||
start_direction: start_direction.to_array(),
|
||||
base_radius: helix.radius,
|
||||
top_radius,
|
||||
height: helix.turns * helix.turn_height,
|
||||
turns: helix.turns,
|
||||
direction: if helix.handedness {
|
||||
HelixDirection::CounterClockwise
|
||||
} else {
|
||||
HelixDirection::Clockwise
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn projected_direction(direction: Vec3, axis: Vec3) -> Option<Vec3> {
|
||||
(direction - axis * direction.dot(axis)).normalize()
|
||||
}
|
||||
|
||||
fn perpendicular(axis: Vec3) -> Option<Vec3> {
|
||||
let basis = if axis.x.abs() <= axis.y.abs() && axis.x.abs() <= axis.z.abs() {
|
||||
Vec3::X
|
||||
} else if axis.y.abs() <= axis.z.abs() {
|
||||
Vec3::Y
|
||||
} else {
|
||||
Vec3::Z
|
||||
};
|
||||
projected_direction(basis, axis)
|
||||
}
|
||||
|
||||
fn rebuild(helix: &mut Helix, top_radius: f64) -> bool {
|
||||
let Some(curve) = kernel_curve(helix, top_radius) else {
|
||||
return false;
|
||||
};
|
||||
let Some(nurbs) = curve.nurbs() else {
|
||||
return false;
|
||||
};
|
||||
helix.spline.degree = nurbs.degree() as i32;
|
||||
helix.spline.knots = nurbs.knots().to_vec();
|
||||
helix.spline.control_points = nurbs
|
||||
.control_points()
|
||||
.iter()
|
||||
.map(|value| Vector3::new(value[0], value[1], value[2]))
|
||||
.collect();
|
||||
helix.spline.weights = if nurbs.is_rational() {
|
||||
nurbs.weights().to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
helix.spline.fit_points.clear();
|
||||
helix.spline.flags.closed = false;
|
||||
helix.spline.flags.periodic = false;
|
||||
helix.spline.flags.rational = nurbs.is_rational();
|
||||
helix.spline.flags.planar = false;
|
||||
helix.spline.flags.linear = false;
|
||||
true
|
||||
}
|
||||
|
||||
fn choice(label: &str, field: &'static str, selected: &str, options: &[String]) -> Property {
|
||||
Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: selected.to_string(),
|
||||
options: options.to_vec(),
|
||||
},
|
||||
}
|
||||
let vx = p.x - base.x;
|
||||
let vy = p.y - base.y;
|
||||
let vz = p.z - base.z;
|
||||
let proj = vx * axis.x + vy * axis.y + vz * axis.z;
|
||||
let px = vx - proj * axis.x;
|
||||
let py = vy - proj * axis.y;
|
||||
let pz = vz - proj * axis.z;
|
||||
Some((px * px + py * py + pz * pz).sqrt())
|
||||
}
|
||||
|
||||
fn grips(helix: &Helix) -> Vec<GripDef> {
|
||||
Grippable::grips(&helix.spline)
|
||||
let Some((base, axis, _)) = axis_frame(helix) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let height = helix.turns * helix.turn_height;
|
||||
let top_center = base + axis * height;
|
||||
let top_endpoint = spline_curve(&helix.spline)
|
||||
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||
.unwrap_or(top_center);
|
||||
vec![
|
||||
center_grip(0, display(base)),
|
||||
square_grip(1, display(point(helix.start_point))),
|
||||
square_grip(2, display(top_center)),
|
||||
square_grip(3, display(top_endpoint)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(helix: &Helix) -> Vec<PropSection> {
|
||||
let bp = &helix.axis_base_point;
|
||||
let turns = helix.turns;
|
||||
let turn_height = helix.turn_height;
|
||||
// Overall height is turns × per-turn rise.
|
||||
let height = turns * turn_height;
|
||||
let base_radius = helix.radius;
|
||||
// Top radius: the axial distance of the last defining point. For a
|
||||
// cylindrical helix this equals the base radius.
|
||||
let top_radius = helix
|
||||
.spline
|
||||
.control_points
|
||||
.last()
|
||||
.or_else(|| helix.spline.fit_points.last())
|
||||
.and_then(|p| radius_from_axis(helix, p))
|
||||
.unwrap_or(base_radius);
|
||||
// Turn slope: rise angle of a single turn against the base circumference.
|
||||
let turn_slope = if base_radius.abs() > 1e-9 {
|
||||
(turn_height / (std::f64::consts::TAU * base_radius))
|
||||
.atan()
|
||||
.to_degrees()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let twist = if helix.handedness { "CW" } else { "CCW" };
|
||||
let base = point(helix.axis_base_point);
|
||||
let height = helix.turns * helix.turn_height;
|
||||
let top_radius = top_radius(helix);
|
||||
let curve = kernel_curve(helix, top_radius);
|
||||
let turn_slope = curve.as_ref().and_then(HelixCurve::turn_slope).unwrap_or(0.0);
|
||||
let total_length = curve.as_ref().and_then(HelixCurve::length).unwrap_or(0.0);
|
||||
let constrain = match helix.constraint {
|
||||
HelixConstraint::TurnHeight => "Turn Height",
|
||||
HelixConstraint::Turns => "Turns",
|
||||
HelixConstraint::Height => "Height",
|
||||
HelixConstraint::TurnHeight => t!("Turn Height").into_owned(),
|
||||
HelixConstraint::Turns => t!("Turns").into_owned(),
|
||||
HelixConstraint::Height => t!("Height").into_owned(),
|
||||
};
|
||||
let twist = if helix.handedness {
|
||||
t!("CCW").into_owned()
|
||||
} else {
|
||||
t!("CW").into_owned()
|
||||
};
|
||||
let constraint_options = vec![
|
||||
t!("Turn Height").into_owned(),
|
||||
t!("Turns").into_owned(),
|
||||
t!("Height").into_owned(),
|
||||
];
|
||||
let twist_options = vec![t!("CW").into_owned(), t!("CCW").into_owned()];
|
||||
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Base point X").as_ref(), "base_x", format!("{:.4}", bp.x)),
|
||||
ro(t!("Base point Y").as_ref(), "base_y", format!("{:.4}", bp.y)),
|
||||
ro(t!("Base point Z").as_ref(), "base_z", format!("{:.4}", bp.z)),
|
||||
ro(t!("Number of turns").as_ref(), "num_turns", format!("{turns:.4}")),
|
||||
ro(t!("Turn height").as_ref(), "turn_height", format!("{turn_height:.4}")),
|
||||
ro(t!("Turns").as_ref(), "turns", format!("{turns:.4}")),
|
||||
ro(t!("Height").as_ref(), "height", format!("{height:.4}")),
|
||||
ro(t!("Base radius").as_ref(), "base_radius", format!("{base_radius:.4}")),
|
||||
ro(t!("Top radius").as_ref(), "top_radius", format!("{top_radius:.4}")),
|
||||
ro(t!("Turn slope").as_ref(), "turn_slope", format!("{turn_slope:.4}")),
|
||||
ro(t!("Twist").as_ref(), "twist", twist),
|
||||
ro(t!("Constrain").as_ref(), "constrain", constrain),
|
||||
edit(t!("Position X").as_ref(), "position_x", base.x),
|
||||
edit(t!("Position Y").as_ref(), "position_y", base.y),
|
||||
edit(t!("Position Z").as_ref(), "position_z", base.z),
|
||||
choice(t!("Constrain").as_ref(), "constrain", &constrain, &constraint_options),
|
||||
edit(t!("Height").as_ref(), "height", height),
|
||||
edit(t!("Turns").as_ref(), "turns", helix.turns),
|
||||
edit(t!("Turn height").as_ref(), "turn_height", helix.turn_height),
|
||||
edit(t!("Base radius").as_ref(), "base_radius", helix.radius),
|
||||
edit(t!("Top radius").as_ref(), "top_radius", top_radius),
|
||||
choice(t!("Twist").as_ref(), "twist", &twist, &twist_options),
|
||||
ro(t!("Turn slope").as_ref(), "turn_slope", format_angle(turn_slope)),
|
||||
ro(t!("Total length").as_ref(), "total_length", format_length(total_length)),
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
/// Helix geometry is derived from the stored spline; the generating
|
||||
/// parameters are read-only (editing one would require regenerating the
|
||||
/// spline, which the loaded entity does not carry a recipe for).
|
||||
fn apply_geom_prop(_helix: &mut Helix, _field: &str, _value: &str) {}
|
||||
|
||||
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
||||
Grippable::apply_grip(&mut helix.spline, grip_id, apply);
|
||||
fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
|
||||
if field == "constrain" {
|
||||
let constraint = if value == t!("Height").as_ref() {
|
||||
HelixConstraint::Height
|
||||
} else if value == t!("Turns").as_ref() {
|
||||
HelixConstraint::Turns
|
||||
} else if value == t!("Turn Height").as_ref() {
|
||||
HelixConstraint::TurnHeight
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
helix.constraint = constraint;
|
||||
return;
|
||||
}
|
||||
let original = helix.clone();
|
||||
let top = top_radius(helix);
|
||||
if field == "twist" {
|
||||
if value == t!("CCW").as_ref() {
|
||||
helix.handedness = true;
|
||||
} else if value == t!("CW").as_ref() {
|
||||
helix.handedness = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
|
||||
return;
|
||||
};
|
||||
let old_base = point(helix.axis_base_point);
|
||||
let old_height = helix.turns * helix.turn_height;
|
||||
match field {
|
||||
"position_x" | "position_y" | "position_z" => {
|
||||
let mut base = old_base;
|
||||
match field {
|
||||
"position_x" => base.x = number,
|
||||
"position_y" => base.y = number,
|
||||
_ => base.z = number,
|
||||
}
|
||||
let delta = base - old_base;
|
||||
helix.axis_base_point = vector(base);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
"height" if number.abs() > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
let turns = number / helix.turn_height;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turns = turns;
|
||||
} else {
|
||||
helix.turn_height = number / helix.turns.max(EPSILON);
|
||||
}
|
||||
}
|
||||
"turns" if number > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::TurnHeight {
|
||||
helix.turns = number;
|
||||
} else {
|
||||
helix.turns = number;
|
||||
helix.turn_height = old_height / number;
|
||||
}
|
||||
}
|
||||
"turn_height" if number.abs() > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::Turns {
|
||||
helix.turn_height = number;
|
||||
} else {
|
||||
let turns = old_height / number;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turn_height = number;
|
||||
helix.turns = turns;
|
||||
}
|
||||
}
|
||||
"base_radius" if number > EPSILON => {
|
||||
let Some((base, _, start_direction)) = axis_frame(helix) else {
|
||||
return;
|
||||
};
|
||||
helix.radius = number;
|
||||
helix.start_point = vector(base + start_direction * number);
|
||||
}
|
||||
"top_radius" if number >= 0.0 => {
|
||||
if !rebuild(helix, number) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(helix: &mut Helix, t: &EntityTransform) {
|
||||
// Move the visible curve, then keep the generating points in step.
|
||||
Transformable::apply_transform(&mut helix.spline, t);
|
||||
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
||||
let original = helix.clone();
|
||||
let top = top_radius(helix);
|
||||
let Some((base, axis, start_direction)) = axis_frame(helix) else {
|
||||
return;
|
||||
};
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Translate(delta)) => {
|
||||
let delta = kernel(delta);
|
||||
if !finite(delta) {
|
||||
return;
|
||||
}
|
||||
helix.axis_base_point = vector(base + delta);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
(0, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let delta = position - base;
|
||||
helix.axis_base_point = vector(position);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
(1, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let radial = position - base - axis * (position - base).dot(axis);
|
||||
let radius = radial.length();
|
||||
if radius <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.radius = radius;
|
||||
helix.start_point = vector(base + radial);
|
||||
}
|
||||
(2, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let axis_vector = position - base;
|
||||
let height = axis_vector.length();
|
||||
if height <= EPSILON {
|
||||
return;
|
||||
}
|
||||
let target_axis = axis_vector / height;
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
helix.axis_vector = vector(target_axis * helix.turn_height.signum());
|
||||
helix.turns = height / helix.turn_height.abs();
|
||||
} else {
|
||||
helix.axis_vector = vector(target_axis);
|
||||
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||
}
|
||||
let new_axis = point(helix.axis_vector);
|
||||
let start_direction = projected_direction(start_direction, new_axis)
|
||||
.or_else(|| perpendicular(new_axis));
|
||||
let Some(start_direction) = start_direction else {
|
||||
*helix = original;
|
||||
return;
|
||||
};
|
||||
helix.start_point = vector(base + start_direction * helix.radius);
|
||||
}
|
||||
(3, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let delta = position - base;
|
||||
let height = delta.dot(axis);
|
||||
let radius = (delta - axis * height).length();
|
||||
if height.abs() <= EPSILON {
|
||||
return;
|
||||
}
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
let turns = height / helix.turn_height;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turns = turns;
|
||||
} else {
|
||||
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||
}
|
||||
if !rebuild(helix, radius) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(helix: &mut Helix, transform: &EntityTransform) {
|
||||
match transform {
|
||||
EntityTransform::Translate(delta) => {
|
||||
acadrust::Entity::translate(helix, Vector3::new(delta.x, delta.y, delta.z));
|
||||
}
|
||||
EntityTransform::Rotate { center, axis, angle_rad } => {
|
||||
crate::scene::view::transform::apply_standard_transform(
|
||||
helix,
|
||||
*center,
|
||||
*axis,
|
||||
*angle_rad,
|
||||
);
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => {
|
||||
crate::scene::view::transform::apply_standard_scale(helix, *center, *factor);
|
||||
}
|
||||
EntityTransform::Mirror { p1, p2, working_normal } => {
|
||||
let reflection = crate::scene::view::transform::reflection_about_working_line(
|
||||
*p1,
|
||||
*p2,
|
||||
*working_normal,
|
||||
);
|
||||
acadrust::Entity::apply_transform(helix, &reflection);
|
||||
}
|
||||
EntityTransform::Affine(value) => acadrust::Entity::apply_transform(helix, value),
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderConvertible for Helix {
|
||||
@@ -113,6 +434,7 @@ impl Grippable for Helix {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
@@ -122,13 +444,14 @@ impl crate::entities::traits::PropertyEditable for Helix {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Helix {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||
apply_transform(self, transform);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+368
-185
@@ -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"] });
|
||||
Reference in new issue
Block a user