From f80e9f742c8aabef44ca9ed694b3841adf1e9aa5 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:25:11 +0300 Subject: [PATCH 1/2] Reverse selected curves while preserving spline shape --- src/app/commands/draw.rs | 20 +++++++++++++++++--- src/modules/draw/modify/reverse.rs | 14 ++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index cf848ff7b..7c5ce76ce 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -596,9 +596,23 @@ impl OpenCADStudio { "REVERSE" => { use crate::modules::draw::modify::reverse::ReverseCommand; - let new_cmd = ReverseCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + if self.tabs[i].scene.selected.is_empty() { + use crate::modules::draw::select::SelectObjectsCommand; + let selection = SelectObjectsCommand::new("REVERSE"); + self.command_line.push_info(&selection.prompt()); + self.tabs[i].active_cmd = Some(Box::new(selection)); + } else { + let replacements = self.tabs[i].scene.selected_entities().into_iter() + .filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle)) + .filter_map(|(handle, entity)| ReverseCommand::reversed(entity) + .map(|reversed| (handle, vec![reversed]))) + .collect::>(); + if !replacements.is_empty() { + return Some(self.apply_cmd_result(crate::command::CmdResult::ReplaceMany( + replacements, Vec::new(), + ))); + } + } } "MEASUREGEOM" => { diff --git a/src/modules/draw/modify/reverse.rs b/src/modules/draw/modify/reverse.rs index 3d7e3d51e..4e2cfdffd 100644 --- a/src/modules/draw/modify/reverse.rs +++ b/src/modules/draw/modify/reverse.rs @@ -54,7 +54,7 @@ impl ReverseCommand { } /// Build a reversed copy of `entity`, or `None` for an unsupported type. - fn reversed(entity: &EntityType) -> Option { + pub fn reversed(entity: &EntityType) -> Option { match entity { EntityType::Line(line) => { let mut out = line.clone(); @@ -176,9 +176,8 @@ fn reverse_polyline2d(pl: &acadrust::entities::Polyline2D) -> acadrust::entities out } -/// Reverse a Spline: flip control points and fit points, regenerate the -/// clamped knot vector. Mirrors `splinedit::apply_spline_op`'s REVERSE branch. -fn reverse_spline(sp: &Spline) -> Spline { +/// Reverse stored coordinates in 3D and let the kernel mirror the knot domain. +pub(super) fn reverse_spline(sp: &Spline) -> Spline { let mut out = sp.clone(); out.control_points.reverse(); out.fit_points.reverse(); @@ -186,8 +185,11 @@ fn reverse_spline(sp: &Spline) -> Spline { if out.weights.len() == out.control_points.len() { out.weights.reverse(); } - out.knots = - Spline::generate_clamped_knots(out.degree as usize, out.control_points.len()); + if let Some(curve) = super::spline_ops::spline_to_nurbs(sp) { + out.knots = curve.reversed().knots().to_vec(); + } + out.begin_tangent = acadrust::types::Vector3::new(-sp.end_tangent.x, -sp.end_tangent.y, -sp.end_tangent.z); + out.end_tangent = acadrust::types::Vector3::new(-sp.begin_tangent.x, -sp.begin_tangent.y, -sp.begin_tangent.z); out } From 25ac594d85c6792b074faa3b96de4920a664a6f9 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:03:58 +0300 Subject: [PATCH 2/2] Reverse complete helix geometry and preserve top radius semantics --- src/entities/helix.rs | 42 +++++++++++++++++++++++------- src/modules/draw/draw/helix.rs | 2 +- src/modules/draw/modify/reverse.rs | 3 ++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/entities/helix.rs b/src/entities/helix.rs index aa753c6e3..ef50e5560 100644 --- a/src/entities/helix.rs +++ b/src/entities/helix.rs @@ -52,10 +52,13 @@ fn spline_curve(spline: &Spline) -> Option { 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)) + let tangent = spline_curve(&helix.spline).map(|curve| curve.tangent_at(0.0)).unwrap_or([0.0;3]); + let (axis, radial, _) = HelixCurve::frame_from_points(base.to_array(),point(helix.axis_vector).to_array(),point(helix.start_point).to_array(),tangent)?; + Some((base,Vec3::from(axis),Vec3::from(radial))) +} + +fn base_radius(helix: &Helix) -> Option { + radius_from_axis(helix, point(helix.start_point)) } fn radius_from_axis(helix: &Helix, value: Vec3) -> Option { @@ -77,7 +80,7 @@ fn kernel_curve(helix: &Helix, top_radius: f64) -> Option { base_center: base.to_array(), axis_direction: axis.to_array(), start_direction: start_direction.to_array(), - base_radius: helix.radius, + base_radius: base_radius(helix)?, top_radius, height: helix.turns * helix.turn_height, turns: helix.turns, @@ -89,6 +92,27 @@ fn kernel_curve(helix: &Helix, top_radius: f64) -> Option { }) } +/// Reverse generating parameters together with the exact stored spline. +pub(crate) fn reversed(helix: &Helix) -> Option { + let original_curve = spline_curve(&helix.spline)?; + let parameters = kernel_curve(helix,top_radius(helix))?.reversed()?; + let mut result = helix.clone(); + result.axis_base_point = vector(Vec3::from(parameters.base_center)); + result.axis_vector = vector(Vec3::from(parameters.axis_direction)); + result.start_point = vector(Vec3::from(original_curve.point_at(1.0))); + result.radius = parameters.top_radius; + // Reverse the evaluated curve itself instead of fitting it again. + let reversed_curve=original_curve.reversed()?; + result.spline.control_points=reversed_curve.control_points().iter().map(|point|Vector3::new(point[0],point[1],point[2])).collect(); + result.spline.fit_points.reverse(); + result.spline.weights=if helix.spline.weights.is_empty(){Vec::new()}else{reversed_curve.weights().to_vec()}; + result.spline.knots=reversed_curve.knots().to_vec(); + let begin=helix.spline.begin_tangent;let end=helix.spline.end_tangent; + result.spline.begin_tangent=Vector3::new(-end.x,-end.y,-end.z); + result.spline.end_tangent=Vector3::new(-begin.x,-begin.y,-begin.z); + Some(result) +} + fn projected_direction(direction: Vec3, axis: Vec3) -> Option { (direction - axis * direction.dot(axis)).normalize() } @@ -111,6 +135,7 @@ fn rebuild(helix: &mut Helix, top_radius: f64) -> bool { let Some(nurbs) = curve.nurbs() else { return false; }; + helix.radius = top_radius; helix.spline.degree = nurbs.degree() as i32; helix.spline.knots = nurbs.knots().to_vec(); helix.spline.control_points = nurbs @@ -194,7 +219,7 @@ fn properties(helix: &Helix) -> Vec { 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!("Base radius").as_ref(), "base_radius", base_radius(helix).unwrap_or(0.0)), 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)), @@ -284,7 +309,6 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) { 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 => { @@ -303,6 +327,7 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) { fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) { let original = helix.clone(); let top = top_radius(helix); + let Some(base_radius) = base_radius(helix) else { return; }; let Some((base, axis, start_direction)) = axis_frame(helix) else { return; }; @@ -334,7 +359,6 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) { if radius <= EPSILON { return; } - helix.radius = radius; helix.start_point = vector(base + radial); } (2, GripApply::Absolute(position)) => { @@ -362,7 +386,7 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) { *helix = original; return; }; - helix.start_point = vector(base + start_direction * helix.radius); + helix.start_point = vector(base + start_direction * base_radius); } (3, GripApply::Absolute(position)) => { let position = kernel(position); diff --git a/src/modules/draw/draw/helix.rs b/src/modules/draw/draw/helix.rs index fbeb2ca63..c081db6eb 100644 --- a/src/modules/draw/draw/helix.rs +++ b/src/modules/draw/draw/helix.rs @@ -134,7 +134,7 @@ impl HelixCommand { 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.radius = curve.top_radius; helix.turns = curve.turns; helix.turn_height = height / curve.turns; helix.handedness = self.counter_clockwise; diff --git a/src/modules/draw/modify/reverse.rs b/src/modules/draw/modify/reverse.rs index 4e2cfdffd..75976031c 100644 --- a/src/modules/draw/modify/reverse.rs +++ b/src/modules/draw/modify/reverse.rs @@ -76,6 +76,7 @@ impl ReverseCommand { Some(EntityType::Polyline3D(out)) } EntityType::Spline(sp) => Some(EntityType::Spline(reverse_spline(sp))), + EntityType::Helix(helix) => crate::entities::helix::reversed(helix).map(EntityType::Helix), _ => None, } } @@ -199,7 +200,7 @@ impl CadCommand for ReverseCommand { } fn prompt(&self) -> String { - t!("REVERSE Select line, polyline or spline to reverse:").into_owned() + t!("REVERSE Select line, polyline, spline or helix to reverse:").into_owned() } fn needs_entity_pick(&self) -> bool {