From bfb649a8bbe741918a88ac21b583d03357154265 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:48:38 +0300 Subject: [PATCH] Align helix properties and editing --- src/entities/helix.rs | 397 +++++++++++++++++++++++++++++++++--------- 1 file changed, 316 insertions(+), 81 deletions(-) diff --git a/src/entities/helix.rs b/src/entities/helix.rs index 2e5bf7d1c..36465ba55 100644 --- a/src/entities/helix.rs +++ b/src/entities/helix.rs @@ -1,106 +1,339 @@ -// 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}; +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 { - 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 { +const EPSILON: f64 = 1.0e-9; + +fn point(vector: Vector3) -> DVec3 { + DVec3::new(vector.x, vector.y, vector.z) +} + +fn vector(value: DVec3) -> Vector3 { + Vector3::new(value.x, value.y, value.z) +} + +fn spline_curve(spline: &Spline) -> Option { + let controls = spline + .control_points + .iter() + .map(|value| [value.x, value.y, value.z]) + .collect(); + let weights = (spline.weights.len() == spline.control_points.len()) + .then(|| spline.weights.clone()); + NurbsCurve3::new(spline.degree.max(1) as usize, controls, spline.knots.clone(), weights) +} + +fn axis_frame(helix: &Helix) -> Option<(DVec3, DVec3, DVec3)> { + let base = point(helix.axis_base_point); + let axis = point(helix.axis_vector).normalize_or_zero(); + if axis.length_squared() <= EPSILON * EPSILON { return None; } - 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()) + let radial = point(helix.start_point) - base; + let start_direction = (radial - axis * radial.dot(axis)).normalize_or_zero(); + if start_direction.length_squared() <= EPSILON * EPSILON { + return None; + } + Some((base, axis, start_direction)) +} + +fn radius_from_axis(helix: &Helix, value: DVec3) -> Option { + 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| DVec3::from_array(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 { + 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 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(), + }, + } } fn grips(helix: &Helix) -> Vec { - 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| DVec3::from_array(curve.point_at(1.0))) + .unwrap_or(top_center); + vec![ + center_grip(0, base), + square_grip(1, point(helix.start_point)), + square_grip(2, top_center), + square_grip(3, top_endpoint), + ] } fn properties(helix: &Helix) -> Vec { - 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" { + helix.constraint = if value == t!("Height").as_ref() { + HelixConstraint::Height + } else if value == t!("Turns").as_ref() { + HelixConstraint::Turns + } else { + HelixConstraint::TurnHeight + }; + return; + } + let top = top_radius(helix); + if field == "twist" { + helix.handedness = value == t!("CCW").as_ref(); + let _ = rebuild(helix, top); + return; + } + let Some(number) = parse_f64(value) 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 { + helix.turns = (number / helix.turn_height).abs().max(EPSILON); + } 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 { + helix.turn_height = number; + helix.turns = (old_height / number).abs().max(EPSILON); + } + } + "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 => { + let _ = rebuild(helix, number); + return; + } + _ => return, + } + let _ = rebuild(helix, top); } -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 top = top_radius(helix); + let Some((base, axis, _)) = axis_frame(helix) else { + return; + }; + match (grip_id, apply) { + (0, GripApply::Translate(delta)) => { + helix.axis_base_point = vector(base + delta); + helix.start_point = vector(point(helix.start_point) + delta); + } + (0, GripApply::Absolute(position)) => { + let delta = position - base; + helix.axis_base_point = vector(position); + helix.start_point = vector(point(helix.start_point) + delta); + } + (1, GripApply::Absolute(position)) => { + 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 axis_vector = position - base; + let height = axis_vector.length(); + if height <= EPSILON { + return; + } + helix.axis_vector = vector(axis_vector / height); + if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON { + helix.turns = (height / helix.turn_height).abs().max(EPSILON); + } else { + helix.turn_height = height / helix.turns.max(EPSILON); + } + } + (3, GripApply::Absolute(position)) => { + 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 { + helix.turns = (height / helix.turn_height).abs().max(EPSILON); + } else { + helix.turn_height = height / helix.turns.max(EPSILON); + } + let _ = rebuild(helix, radius); + return; + } + _ => return, + } + let _ = rebuild(helix, top); +} + +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 +346,7 @@ impl Grippable for Helix { fn grips(&self) -> Vec { grips(self) } + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { apply_grip(self, grip_id, apply); } @@ -122,13 +356,14 @@ impl crate::entities::traits::PropertyEditable for Helix { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { 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); } }